0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

生成AI時代のMarkdownビューアを、自分好みに整えるCSS設計

0
Last updated at Posted at 2026-05-02

エグゼクティブサマリー

この記事は、Codex CLI / Claude Code を日常的に使うようになって、Markdownファイルを読む量が増えすぎたので、MarkdownをHTML化したときの読み面を自分好みに整えた話です。
Markdownは構造化されたテキストなので、生成AIとの相性はかなりいいです。設計メモ、レビュー結果、作業ログ、仕様整理、調査メモがMarkdownで残るのは便利です。ただ、そのぶん読む文章が一気に増えます。

そこで、Markdown本文はなるべくそのままにして、HTML変換時に <style> と少しの <script> を差し込む形にしました。目的は、Markdownを派手にすることではなく、長いMarkdownを疲れにくく読むためのビューアに寄せることです。

結論はこの4つです。

  • Markdown本文は汚さない
  • HTML化後の読み面だけ整える
  • 目次、進捗、設定、コピー、PDF出力を足す
  • 自分のローカルHTML変換環境で使いやすい形にする

結論

Markdownが増える時代は、Markdownの書き方だけでなく、Markdownを読むUIも整えたほうがいいです。
今回の形では、Markdownファイルの末尾に <style><script> を置いています。

MarkdownをHTMLに変換したとき、そのHTML側にCSSとJavaScriptがそのまま入る変換環境なら、本文に読書用のレイアウトや操作ボタンが乗ります。
こういう感じ。

## 記事本文

本文を書く。

<style>
  読みやすくするCSS
</style>

<script>
  目次、設定、コピー、PDF出力などのJS
</script>

記事本文側は普通のMarkdownのままです。見た目や操作感は、HTML化したあとのレイヤーで整えます。

背景

Codex CLI や Claude Code を通常業務で使うのが当たり前になってくると、Markdownファイルがかなり増えます。
要件整理、実装方針、レビュー観点、調査メモ、作業ログ、プロンプトの控えなど、生成AIとやり取りするものはMarkdownに落ち着きやすいです。構造化されていて、差分も追いやすく、あとから検索もしやすい。これはかなり助かります。

ただ、困るのは読む量です。
Markdownは便利ですが、長い文章を素のプレビューで読み続けると、だんだん目が疲れます。コードブロックも多いし、見出しも深くなるし、どこまで読んだか分かりにくくなることもあります。

そこで、自分用のMarkdownビューアとして、HTML変換後に読みやすさを足すCSSとJSを作りました。

補足:想定している使い方

想定しているのは、ローカルや自分の管理下にある変換環境です。

  • MarkdownをHTMLに変換する
  • 変換時にMarkdown内のHTMLタグが残る
  • 生成されたHTMLをブラウザで読む
  • 長い記事や設計メモを読む機会が多い

投稿サービスによっては <style><script> が削除されます。特に <script> は制限されることが多いので、公開ブログにそのまま貼るというより、自分のHTML変換フローで使う前提です。

やったこと

やったことは、Markdownを「読むための画面」に寄せることです。
本文そのものを書き換えるのではなく、HTML化後に記事ルートを探して、.md-article として読みやすい領域を作ります。そのうえで、固定ヘッダー、目次、読書設定、コードコピーなどを追加しています。

機能はこのあたりです。

機能 何をしているか
記事ルートの自動検出 .znc.markdown-bodyarticlemain.content などを探して、本文領域として扱います
汎用Markdownレイアウト 本文を白い読み面に載せ、余白、行間、見出し、引用、表、コードブロックを整えます
ライト / ダーク切り替え OS設定を見つつ、ボタンでテーマを切り替えます。選択は localStorage に保存します
固定ヘッダー ページ上部に記事タイトルと操作ボタンを固定表示します
右側目次 h1 から h4 を拾って目次を作り、現在位置の見出しをハイライトします
読書進捗バー ページ上部に、どこまで読んだかを細いバーで表示します
読書設定 文字サイズ、行間、モーション控えめ、目次表示を切り替えられます
コードコピー pre > code にコピーボタンを追加し、成功時と失敗時の表示も変えます
ページ上部へ戻る スクロール後に丸いボタンを出して、上部へ戻れるようにします
セクション表示演出 h2 ごとにセクションをまとめ、スクロール時に軽く表示演出を入れます
PDF出力 印刷ダイアログを開く前にライトテーマへ寄せ、UI部品を隠して出力しやすくします
印刷用CSS A4、余白、改ページ、コードブロックや表の扱いを調整します
レスポンシブ対応 PCでは右目次を出し、狭い画面では目次を隠して本文優先にします
Zenn系レイアウト補正 Zennの記事コンテナ幅に引っ張られすぎないよう、親コンテナ側も一部補正します
アクセシビリティ寄りの属性 ボタンに aria-labelaria-pressedaria-expanded などを付けています

全部を大げさなアプリにするのではなく、Markdownの読みづらさに効くところだけを足す感じです。

補足:特に効いたところ

個人的に効いたのは、右側目次、読書進捗、コードコピー、文字サイズ変更です。
生成AIが出すMarkdownは、どうしても見出しが多くなります。目次が常に見えるだけで、文章の中で迷いにくくなります。

コードブロックも多いので、コピーボタンは地味に大事です。READMEや設計メモを読みながらコマンドを拾うときに、毎回ドラッグしなくてよくなります。
文字サイズと行間の切り替えは、長文を読むときに効きます。昼は標準、夜は少し大きめ、のように変えられるだけでも負担が違います。

最終形

最終的には、CSS変数で見た目をまとめ、JavaScriptで必要なUIだけ後付けする形に落ち着きました。
考え方としては、次のように分けています。

  • CSS変数で色、余白、幅、フォントを調整する
  • Markdown本文のセマンティクスはなるべく触らない
  • JavaScriptは目次やボタンなど、操作に必要な部分だけを作る
  • 設定値は localStorage に保存して、次回も同じ読み心地にする
  • 印刷やPDF出力時は、読むためのUIを隠す

調整したい場合は、まずこのあたりを見ると扱いやすいです。

調整したいもの 見る場所
本文の最大幅 --md-content-max
右側目次の幅 --md-toc-width
画面全体の余白 --md-page-pad
アクセントカラー --md-accent
背景色や本文色 --md-bg--md-surface--md-text
記事カードの余白 .md-article
文字サイズの選択肢 READING_SETTING_OPTIONS.font
行間の選択肢 READING_SETTING_OPTIONS.line
目次を隠す幅 @media (max-width: 1099px)
印刷時の余白 @page

CSSを全部読み切らなくても、変数だけ触ればかなり印象を変えられます。
実際のMarkdownにセットするソースコードは以下。

<style>
  :root {
    --md-font-base: -apple-system, BlinkMacSystemFont, "Hiragino Kaku Gothic ProN", "Hiragino Sans", Meiryo, sans-serif, "Segoe UI Emoji";
    --md-font-code: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace, "Segoe UI Emoji";
    --md-font-latin-hero: "Segoe UI", BlinkMacSystemFont, Arial, sans-serif;
    --md-bg: #f8fafc;
    --md-surface: #ffffff;
    --md-text: #1f2937;
    --md-muted: #6b7280;
    --md-border: #e5e7eb;
    --md-border-strong: #d1d5db;
    --md-panel-bg: rgba(255, 255, 255, 0.94);
    --md-code-bg: #f6f8fa;
    --md-code-text: #1f2937;
    --md-link: #0f83fd;
    --md-accent: #3ea8ff;
    --md-accent-soft: rgba(62, 168, 255, 0.12);
    --md-button-bg: #111827;
    --md-button-text: #ffffff;

    --md-toc-width: 288px;
    --md-toc-right: 16px;
    --md-toc-gap: 28px;
    --md-page-pad: clamp(16px, 3vw, 40px);
    --md-right-reserve: calc(
      var(--md-toc-width) + var(--md-toc-right) + var(--md-toc-gap)
    );

    --md-content-max: 1120px;
  }

  @media (prefers-color-scheme: dark) {
    :root:not([data-md-theme]) {
      --md-bg: #0f172a;
      --md-surface: #111827;
      --md-text: #e5e7eb;
      --md-muted: #9ca3af;
      --md-border: #263244;
      --md-border-strong: #334155;
      --md-panel-bg: rgba(15, 23, 42, 0.94);
      --md-code-bg: #111827;
      --md-code-text: #e5e7eb;
      --md-link: #60a5fa;
      --md-accent: #3ea8ff;
      --md-accent-soft: rgba(62, 168, 255, 0.16);
      --md-button-bg: #e5e7eb;
      --md-button-text: #111827;
    }
  }

  html[data-md-theme="dark"] {
    color-scheme: dark;
    --md-bg: #0f172a;
    --md-surface: #111827;
    --md-text: #e5e7eb;
    --md-muted: #9ca3af;
    --md-border: #263244;
    --md-border-strong: #334155;
    --md-panel-bg: rgba(15, 23, 42, 0.94);
    --md-code-bg: #111827;
    --md-code-text: #e5e7eb;
    --md-link: #60a5fa;
    --md-button-bg: #e5e7eb;
    --md-button-text: #111827;
  }

  html[data-md-theme="light"] {
    color-scheme: light;
  }

  html {
    scroll-behavior: smooth;
  }

  body {
    background:
      radial-gradient(circle at top left, var(--md-accent-soft), transparent 30rem),
      var(--md-bg);
    color: var(--md-text);
    transition:
      background-color 0.2s ease,
      color 0.2s ease,
      padding-right 0.2s ease;
  }

  #md-reading-progress {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 10001;
    width: 0%;
    height: 3px;
    background: linear-gradient(90deg, var(--md-accent), #7dd3fc);
    transition: width 0.08s linear;
  }

  @media (min-width: 1100px) {
    html.md-toc-visible body {
      box-sizing: border-box;
      padding-right: var(--md-right-reserve) !important;
      overflow-x: hidden;
    }

    html.md-toc-visible :where(
      [class*="View_columnsContainer__"],
      [class*="View_content__"],
      [class*="View_main__"],
      [class*="Container_default__"]
    ) {
      width: 100% !important;
      max-width: none !important;
    }

    html.md-toc-visible :where([class*="View_main__"]) {
      margin-left: 0 !important;
      margin-right: 0 !important;
    }

    html.md-toc-visible :where([class*="Container_default__"]) {
      box-sizing: border-box !important;
      width: min(
        var(--md-content-max),
        calc(100vw - var(--md-right-reserve) - var(--md-page-pad) * 2)
      ) !important;
      max-width: min(
        var(--md-content-max),
        calc(100vw - var(--md-right-reserve) - var(--md-page-pad) * 2)
      ) !important;
      margin-left: auto !important;
      margin-right: auto !important;
    }

    html.md-toc-visible :where(
      main,
      article,
      .markdown-body,
      .content,
      .znc
    ) {
      box-sizing: border-box;
      width: 100% !important;
      max-width: none !important;
    }

    html.md-toc-visible :where(
      .znc p,
      .markdown-body p,
      .content p
    ) {
      max-width: none !important;
    }

    html.md-toc-visible :where(
      .znc pre,
      .markdown-body pre,
      .content pre,
      .znc .code-block-container,
      .markdown-body .code-block-container,
      .content .code-block-container
    ) {
      max-width: 100% !important;
    }
  }

  @media (min-width: 1440px) {
    :root {
      --md-content-max: 1180px;
    }
  }

  main,
  article,
  .markdown-body,
  .content,
  .znc {
    color: var(--md-text);
  }

  :where(.markdown-body, .content, .znc) {
    line-height: 1.95;
    letter-spacing: 0.01em;
    font-feature-settings: "palt";
  }

  :where(.markdown-body, .content, .znc) p {
    margin: 1.05em 0;
    font-size: 16.5px;
  }

  :where(.markdown-body, .content, .znc) a,
  a {
    color: var(--md-link);
    text-decoration: none;
    text-underline-offset: 0.2em;
  }

  :where(.markdown-body, .content, .znc) a:hover,
  a:hover {
    text-decoration: underline;
  }

  :where(.markdown-body, .content, .znc) strong {
    color: var(--md-text);
    font-weight: 700;
  }

  :where(.markdown-body, .content, .znc) h1,
  :where(.markdown-body, .content, .znc) h2,
  :where(.markdown-body, .content, .znc) h3,
  :where(.markdown-body, .content, .znc) h4 {
    color: var(--md-text);
    font-weight: 800;
    line-height: 1.45;
    scroll-margin-top: 84px;
  }

  :where(.markdown-body, .content, .znc) h1 {
    margin: 0 0 1.3em;
    font-size: clamp(1.9rem, 4vw, 2.7rem);
    letter-spacing: -0.03em;
  }

  :where(.markdown-body, .content, .znc) h2 {
    margin: 3.2em 0 1em;
    padding: 0.15em 0 0.15em 0.75em;
    border-left: 5px solid var(--md-accent);
    font-size: clamp(1.45rem, 2.4vw, 1.9rem);
  }

  :where(.markdown-body, .content, .znc) h3 {
    margin: 2.4em 0 0.85em;
    padding-bottom: 0.35em;
    border-bottom: 1px solid var(--md-border);
    font-size: clamp(1.18rem, 1.8vw, 1.45rem);
  }

  :where(.markdown-body, .content, .znc) h4 {
    margin: 2em 0 0.7em;
    font-size: 2rem;
  }

  :where(.markdown-body, .content, .znc) ul,
  :where(.markdown-body, .content, .znc) ol {
    padding-left: 1.45em;
  }

  :where(.markdown-body, .content, .znc) li + li {
    margin-top: 0.35em;
  }

  :where(.markdown-body, .content, .znc) blockquote {
    margin: 1.5em 0;
    padding: 1em 1.2em;
    border-left: 5px solid var(--md-accent);
    border-radius: 0 12px 12px 0;
    background: var(--md-accent-soft);
    color: var(--md-text);
  }

  :where(.markdown-body, .content, .znc) :not(pre) > code {
    padding: 0.18em 0.42em;
    border: 1px solid var(--md-border);
    border-radius: 6px;
    background: var(--md-code-bg);
    color: var(--md-code-text);
    font-size: 0.9em;
  }

  pre {
    position: relative;
    background: var(--md-code-bg);
    border: 1px solid var(--md-border);
    border-radius: 14px;
    padding: 1.05rem;
    overflow: auto;
    box-shadow: 0 10px 28px rgba(15, 23, 42, 0.08);
  }

  .code-block-container pre,
  pre.shiki {
    border-color: rgba(148, 163, 184, 0.26);
    border-radius: 16px;
  }

  pre code {
    color: inherit;
    font-size: 0.92rem;
    line-height: 1.75;
  }

  :where(.markdown-body, .content, .znc) table {
    display: block;
    width: 100%;
    overflow: auto;
    border-collapse: collapse;
    margin: 1.6em 0;
  }

  :where(.markdown-body, .content, .znc) th,
  :where(.markdown-body, .content, .znc) td {
    padding: 0.75em 0.9em;
    border: 1px solid var(--md-border);
  }

  :where(.markdown-body, .content, .znc) th {
    background: var(--md-code-bg);
    font-weight: 700;
  }

  :where(.markdown-body, .content, .znc) details {
    margin: 1.4em 0;
    padding: 0.9em 1.1em;
    border: 1px solid var(--md-border);
    border-radius: 14px;
    background: var(--md-surface);
  }

  :where(.markdown-body, .content, .znc) summary {
    cursor: pointer;
    font-weight: 700;
  }

  .md-copy-btn {
    position: absolute;
    top: 10px;
    right: 10px;
    border: 1px solid var(--md-border);
    border-radius: 999px;
    padding: 4px 11px;
    font-size: 12px;
    line-height: 1.4;
    color: var(--md-button-text);
    background: var(--md-button-bg);
    cursor: pointer;
    opacity: 0.84;
  }

  .md-copy-btn:hover {
    opacity: 1;
    transform: translateY(-1px);
  }

  #md-floating-controls {
    position: fixed;
    top: 12px;
    right: 12px;
    z-index: 10000;
  }

  #md-theme-toggle {
    display: grid;
    place-items: center;
    width: 36px;
    height: 36px;
    border: 1px solid var(--md-border);
    border-radius: 999px;
    color: var(--md-button-text);
    background: var(--md-button-bg);
    cursor: pointer;
    box-shadow: 0 10px 28px rgba(15, 23, 42, 0.18);
    opacity: 0.92;
  }

  #md-theme-toggle:hover {
    opacity: 1;
    transform: translateY(-1px);
  }

  #md-theme-toggle svg {
    width: 17px;
    height: 17px;
    display: block;
  }

  #md-toc {
    position: fixed;
    top: 58px;
    right: var(--md-toc-right);
    z-index: 9998;
    width: var(--md-toc-width);
    max-height: calc(100vh - 80px);
    overflow: auto;
    border: 1px solid var(--md-border);
    border-radius: 16px;
    background: var(--md-panel-bg);
    backdrop-filter: blur(10px);
    box-shadow: 0 14px 34px rgba(15, 23, 42, 0.13);
    padding: 12px;
    font-size: 13px;
  }

  #md-toc a {
    display: block;
    padding: 6px 8px;
    color: var(--md-muted);
    text-decoration: none;
    border-left: 3px solid transparent;
    border-radius: 9px;
    line-height: 1.55;
  }

  #md-toc a:hover {
    color: var(--md-text);
    background: var(--md-code-bg);
  }

  #md-toc a.is-active {
    color: var(--md-accent);
    background: var(--md-accent-soft);
    border-left-color: var(--md-accent);
    font-weight: 700;
  }

  #md-toc a[data-level="3"] {
    padding-left: 20px;
    font-size: 12px;
  }

  #md-toc a[data-level="4"] {
    padding-left: 32px;
    font-size: 12px;
  }

  @media (max-width: 1099px) {
    html.md-toc-visible body {
      padding-right: 0 !important;
    }

    :where(.markdown-body, .content, .znc) {
      padding-inline: 16px;
    }

    #md-toc {
      display: none;
    }

    #md-floating-controls {
      top: 8px;
      right: 8px;
    }
  }

  :root {
    --md-bg: #edf2f7;
    --md-surface: #ffffff;
    --md-surface-soft: #f8fafc;
    --md-text: #334155;
    --md-heading: #1f2937;
    --md-muted: #64748b;
    --md-border: #dfe7ef;
    --md-border-strong: #cbd5e1;
    --md-panel-bg: rgba(255, 255, 255, 0.97);
    --md-code-text: #334155;
    --md-accent-soft: #eaf6ff;
    --md-button-bg: #2f3a45;
    --md-toc-width: 300px;
    --md-toc-gap: 32px;
    --md-page-pad: clamp(18px, 3vw, 48px);
    --md-right-reserve: calc(
      var(--md-toc-width) + var(--md-toc-right) + var(--md-toc-gap)
    );
  }

  @media (prefers-color-scheme: dark) {
    :root:not([data-md-theme]) {
      --md-bg: #0f172a;
      --md-surface: #111827;
      --md-surface-soft: #0f172a;
      --md-text: #e5e7eb;
      --md-heading: #f8fafc;
      --md-muted: #9ca3af;
      --md-border: #263244;
      --md-border-strong: #334155;
      --md-panel-bg: rgba(15, 23, 42, 0.96);
      --md-code-bg: #111827;
      --md-code-text: #e5e7eb;
      --md-link: #60a5fa;
      --md-accent: #3ea8ff;
      --md-accent-soft: rgba(62, 168, 255, 0.16);
      --md-button-bg: #e5e7eb;
      --md-button-text: #111827;
    }
  }

  html[data-md-theme="dark"] {
    --md-surface-soft: #0f172a;
    --md-heading: #f8fafc;
    --md-accent: #3ea8ff;
    --md-accent-soft: rgba(62, 168, 255, 0.16);
  }

  body {
    box-sizing: border-box;
    min-height: 100vh;
    margin: 0;
    padding: 0 var(--md-page-pad) 56px !important;
    background: var(--md-bg) !important;
    color: var(--md-text);
    font-family: var(--md-font-base);
    font-size: 16px;
    line-height: 1.85;
  }

  @media (min-width: 1100px) {
    html.md-toc-visible body {
      padding: 0 calc(var(--md-right-reserve) + var(--md-page-pad)) 56px var(--md-page-pad) !important;
      overflow-x: hidden;
    }
  }

  .md-article {
    box-sizing: border-box;
    width: 100%;
    max-width: none;
    margin: 40px 0 96px;
    padding: 44px clamp(28px, 4vw, 72px) 56px;
    border: 1px solid rgba(203, 213, 225, 0.74);
    border-radius: 8px;
    background: var(--md-surface);
    box-shadow: 0 16px 42px rgba(15, 23, 42, 0.06);
  }

  :where(.md-article, .markdown-body, .content, .znc) {
    color: var(--md-text);
    font-family: var(--md-font-base);
    line-height: 1.9;
    letter-spacing: 0;
    font-feature-settings: "palt";
  }

  :where(pre, code, kbd, samp) {
    font-family: var(--md-font-code);
  }

  :where(.md-article, .markdown-body, .content, .znc) p {
    margin: 1.15em 0;
    font-size: 16px;
  }

  :where(.md-article, .markdown-body, .content, .znc) strong {
    color: var(--md-heading);
    font-weight: 700;
  }

  :where(.md-article, .markdown-body, .content, .znc) h1,
  :where(.md-article, .markdown-body, .content, .znc) h2,
  :where(.md-article, .markdown-body, .content, .znc) h3,
  :where(.md-article, .markdown-body, .content, .znc) h4 {
    color: var(--md-heading);
    font-weight: 800;
    line-height: 1.5;
    letter-spacing: 0;
    scroll-margin-top: 84px;
  }

  :where(.md-article, .markdown-body, .content, .znc) h1 {
    margin: 0 0 1.6em;
    padding: 0;
    border: 0;
    font-size: 2rem;
  }

  :where(.md-article, .markdown-body, .content, .znc) h2 {
    margin: 3em 0 1em;
    padding: 0 0 0.45em;
    border: 0;
    border-bottom: 1px solid var(--md-border);
    font-size: 1.45rem;
  }

  :where(.md-article, .markdown-body, .content, .znc) h3 {
    margin: 2.4em 0 0.85em;
    padding-bottom: 0.35em;
    border-bottom: 1px solid var(--md-border);
    font-size: 1.2rem;
  }

  :where(.md-article, .markdown-body, .content, .znc) hr {
    height: 0;
    margin: 2.6em 0;
    border: 0;
    border-top: 1px solid var(--md-border);
  }

  :where(.md-article, .markdown-body, .content, .znc) blockquote {
    border-left: 4px solid var(--md-border-strong);
    border-radius: 0 8px 8px 0;
    background: var(--md-surface-soft);
    color: var(--md-muted);
  }

  pre {
    border-radius: 8px;
    box-shadow: none;
  }

  .code-block-container pre,
  pre.shiki,
  :where(.md-article, .markdown-body, .content, .znc) details {
    border-radius: 8px;
  }

  #md-floating-controls {
    top: 12px;
    right: var(--md-toc-right);
  }

  #md-theme-toggle {
    box-shadow: 0 8px 18px rgba(15, 23, 42, 0.12);
  }

  #md-toc {
    top: 58px;
    bottom: 16px;
    right: var(--md-toc-right);
    display: flex;
    flex-direction: column;
    width: var(--md-toc-width);
    max-height: none;
    overflow: hidden;
    border-radius: 8px;
    box-shadow: 0 12px 28px rgba(15, 23, 42, 0.1);
    padding: 14px 10px 10px;
  }

  #md-toc .md-toc-title {
    margin: 0 4px;
    padding-bottom: 10px;
    border-bottom: 1px solid var(--md-border);
    color: var(--md-muted);
    font-weight: 800;
    user-select: none;
  }

  #md-toc .md-toc-links {
    flex: 1 1 auto;
    min-height: 0;
    margin-top: 10px;
    padding-left: 8px;
    border-left: 1px solid var(--md-border);
    overflow: auto;
  }

  #md-toc a {
    position: relative;
    border-left: 0;
    border-radius: 6px;
  }

  #md-toc a::before {
    content: "";
    position: absolute;
    top: 8px;
    bottom: 8px;
    left: -9px;
    width: 2px;
    border-radius: 999px;
    background: transparent;
  }

  #md-toc a.is-active {
    border-left-color: transparent;
  }

  #md-toc a.is-active::before {
    background: var(--md-accent);
  }

  @media (max-width: 1099px) {
    html.md-toc-visible body {
      padding: 0 var(--md-page-pad) 56px !important;
    }

    .md-article {
      margin: 20px 0 56px;
      padding: 28px 18px 36px;
    }

    :where(.md-article, .markdown-body, .content, .znc) h1 {
      font-size: 1.65rem;
    }

    :where(.md-article, .markdown-body, .content, .znc) h2 {
      font-size: 1.28rem;
    }

    #md-floating-controls {
      top: 8px;
      right: 8px;
    }

    #md-toc {
      display: none;
    }
  }

  /* Pattern 2: airy article and wider toc */
  :root {
    --md-fixed-header-height: 64px;
    --md-layout-gap: 24px;
    --md-layout-bottom: 24px;
    --md-layout-top: calc(var(--md-fixed-header-height) + var(--md-layout-gap));
    --md-toc-width: 320px;
    --md-toc-gap: 40px;
    --md-content-max: 1080px;
  }

  .md-article {
    padding: 52px clamp(36px, 5vw, 76px) 64px;
  }

  #md-toc {
    padding: 18px 12px 12px;
  }

  #md-toc .md-toc-title {
    padding-bottom: 12px;
  }
  #md-floating-controls {
    inset: 0 0 auto 0 !important;
    box-sizing: border-box;
    display: flex !important;
    align-items: center;
    justify-content: flex-end;
    width: auto;
    height: var(--md-fixed-header-height);
    padding: 0 var(--md-toc-right) 0 var(--md-page-pad);
    border-bottom: 1px solid var(--md-border);
    background: var(--md-panel-bg);
    box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
    backdrop-filter: blur(12px);
  }

  #md-theme-toggle {
    flex: 0 0 auto;
    box-shadow: none;
  }

  body {
    padding-top: var(--md-layout-top) !important;
  }

  .md-article {
    margin-top: 0 !important;
    min-height: calc(100vh - var(--md-layout-top) - var(--md-layout-bottom));
  }

  #md-toc {
    top: var(--md-layout-top) !important;
    bottom: var(--md-layout-bottom) !important;
    height: auto;
  }

  #md-toc .md-toc-links {
    scrollbar-gutter: stable;
  }

  @media (min-width: 1100px) {
    html.md-toc-visible body {
      padding: var(--md-layout-top)
        calc(var(--md-right-reserve) + var(--md-page-pad))
        var(--md-layout-bottom)
        var(--md-page-pad) !important;
    }

    .md-article {
      margin-bottom: var(--md-layout-bottom) !important;
    }
  }

  @media (max-width: 1099px) {
    html.md-toc-visible body {
      padding: var(--md-layout-top) var(--md-page-pad) 56px !important;
    }

    #md-floating-controls {
      height: var(--md-fixed-header-height);
      padding: 0 12px;
    }

    .md-article {
      min-height: auto;
      margin: 0 0 56px !important;
    }
  }

  #md-theme-toggle {
    position: relative;
    display: flex;
    justify-content: center;
    align-items: center;
    width: 44px;
    height: 44px;
    border: 2px solid var(--md-accent);
    border-radius: 50%;
    background-color: #ffffff;
    color: var(--md-accent);
    font-size: 0.72rem;
    font-weight: 800;
    letter-spacing: 0.04em;
    line-height: 1;
    opacity: 1;
    overflow: hidden;
    animation: md-theme-pulse var(--md-icon-pulse-duration) infinite;
    transition:
      transform 0.3s ease,
      box-shadow 0.3s ease,
      background-color 0.3s ease,
      color 0.3s ease,
      border-color 0.3s ease;
  }

  #md-theme-toggle:hover {
    transform: scale(1.1) rotate(360deg);
    background-color: var(--md-accent);
    color: #ffffff;
    box-shadow: 0 0 15px rgba(62, 168, 255, 0.7);
    animation-play-state: paused;
  }

  #md-theme-toggle.is-night-on {
    background-color: var(--md-accent);
    color: #ffffff;
    border-color: var(--md-accent);
  }

  #md-theme-toggle.is-night-on:hover {
    background-color: #60a5fa;
    border-color: #60a5fa;
  }

  #md-theme-toggle::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 0;
    height: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.45);
    opacity: 0;
    transform: translate(-50%, -50%);
    pointer-events: none;
  }

  #md-theme-toggle.is-toggling::after {
    animation: md-theme-ripple 0.45s ease-out;
  }

  .md-theme-toggle-icon {
    position: relative;
    z-index: 1;
    display: grid;
    place-items: center;
    width: 20px;
    height: 20px;
  }

  .md-theme-toggle-icon svg {
    display: block;
    width: 20px;
    height: 20px;
  }

  #md-scroll-top {
    position: fixed;
    right: 14px;
    bottom: 28px;
    z-index: 9999;
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    border: 2px solid var(--md-accent);
    border-radius: 50%;
    background-color: #ffffff;
    color: var(--md-accent);
    cursor: pointer;
    opacity: 0;
    overflow: hidden;
    pointer-events: none;
    transform: translateY(12px) scale(0.96);
    animation: md-scroll-top-pulse var(--md-icon-pulse-duration) infinite;
    transition:
      opacity 0.24s ease,
      transform 0.3s ease,
      box-shadow 0.3s ease,
      background-color 0.3s ease,
      color 0.3s ease,
      border-color 0.3s ease;
  }

  #md-scroll-top.is-visible {
    opacity: 1;
    pointer-events: auto;
    transform: translateY(0) scale(1);
  }

  #md-scroll-top:hover {
    transform: translateY(-2px) scale(1.08) rotate(360deg);
    background-color: var(--md-accent);
    color: #ffffff;
    box-shadow: 0 0 15px rgba(62, 168, 255, 0.7);
    animation-play-state: paused;
  }

  #md-scroll-top::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 0;
    height: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.45);
    opacity: 0;
    transform: translate(-50%, -50%);
    pointer-events: none;
  }

  #md-scroll-top.is-toggling::after {
    animation: md-theme-ripple 0.45s ease-out;
  }

  .md-scroll-top-icon {
    position: relative;
    z-index: 1;
    display: grid;
    place-items: center;
    width: 20px;
    height: 20px;
  }

  .md-scroll-top-icon svg {
    display: block;
    width: 20px;
    height: 20px;
  }

  html[data-md-theme="dark"] #md-scroll-top {
    background: rgba(15, 23, 42, 0.96);
  }

  @keyframes md-theme-pulse {
    0% {
      box-shadow: 0 0 0 0 rgba(62, 168, 255, 0.58);
    }

    70% {
      box-shadow: 0 0 0 10px rgba(62, 168, 255, 0);
    }

    100% {
      box-shadow: 0 0 0 0 rgba(62, 168, 255, 0);
    }
  }

  @keyframes md-theme-ripple {
    0% {
      width: 0;
      height: 0;
      opacity: 0.55;
    }

    100% {
      width: 80px;
      height: 80px;
      opacity: 0;
    }
  }

  @keyframes md-scroll-top-pulse {
    0% {
      box-shadow: 0 0 0 0 rgba(62, 168, 255, 0.42);
    }

    70% {
      box-shadow: 0 0 0 10px rgba(62, 168, 255, 0);
    }

    100% {
      box-shadow: 0 0 0 0 rgba(62, 168, 255, 0);
    }
  }

  @keyframes md-copy-pulse {
    0% {
      box-shadow: 0 8px 18px rgba(15, 23, 42, 0.1), 0 0 0 0 rgba(62, 168, 255, 0.32);
    }

    70% {
      box-shadow: 0 8px 18px rgba(15, 23, 42, 0.1), 0 0 0 8px rgba(62, 168, 255, 0);
    }

    100% {
      box-shadow: 0 8px 18px rgba(15, 23, 42, 0.1), 0 0 0 0 rgba(62, 168, 255, 0);
    }
  }

  @media (max-width: 1099px) {
    #md-scroll-top {
      right: 14px;
      bottom: 18px;
      left: auto !important;
    }
  }

  /* Header title size pattern 1 */
  #md-floating-controls {
    gap: 18px;
    justify-content: space-between !important;
  }

  #md-fixed-title {
    position: relative;
    flex: 1 1 auto;
    min-width: 0;
    padding-left: 16px;
    color: var(--md-text);
    font-family: var(--md-font-base);
    line-height: 1.25;
    pointer-events: none;
  }

  #md-fixed-title::before {
    content: "";
    position: absolute;
    left: 0;
    top: 50%;
    width: 4px;
    height: 1.75em;
    border-radius: 999px;
    background: var(--md-accent);
    transform: translateY(-50%);
  }

  #md-fixed-title .md-fixed-title-main {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 1.25rem;
    font-weight: 800;
  }

  @media (max-width: 1099px) {
    #md-fixed-title {
      padding-left: 12px;
    }

    #md-fixed-title .md-fixed-title-main {
      font-size: 0.92rem;
    }
  }

  .md-file-title {
    margin: 0 0 1.45em;
    color: var(--md-heading);
    font-family: var(--md-font-base);
    font-size: clamp(1.75rem, 3.4vw, 2.35rem);
    font-weight: 800;
    line-height: 1.45;
    letter-spacing: 0;
  }

  .md-file-title + :where(h1, h2, h3, h4) {
    margin-top: 2.2em !important;
  }

  #md-toc a[data-level="1"] {
    padding-left: 8px;
    color: var(--md-text);
    font-size: 13px;
    font-weight: 800;
  }

  #md-toc a[data-level="2"] {
    padding-left: 20px;
    font-size: 13px;
  }

  #md-toc a[data-level="3"] {
    padding-left: 32px;
    font-size: 12px;
  }

  #md-toc a[data-level="4"] {
    padding-left: 44px;
    font-size: 12px;
  }

  /* Copy button pattern 4: night/back-top family */
  .md-copy-btn {
    position: absolute;
    top: 12px;
    right: 12px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    padding: 0;
    border: 2px solid var(--md-accent);
    border-radius: 50%;
    background: #ffffff;
    color: var(--md-accent);
    box-shadow: 0 8px 18px rgba(15, 23, 42, 0.1);
    opacity: 1;
    overflow: hidden;
    animation: md-copy-pulse var(--md-icon-pulse-duration) infinite;
    transition:
      transform 0.3s ease,
      box-shadow 0.3s ease,
      background-color 0.3s ease,
      color 0.3s ease,
      border-color 0.3s ease;
  }

  .md-copy-btn:hover {
    background: var(--md-accent);
    color: #ffffff;
    box-shadow: 0 0 15px rgba(62, 168, 255, 0.7);
    transform: scale(1.08) rotate(360deg);
    animation-play-state: paused;
  }

  .md-copy-btn.is-pressing {
    transform: scale(0.94);
    animation-play-state: paused;
  }

  .md-copy-btn.is-copied {
    border-color: #22c55e;
    background: #22c55e;
    color: #ffffff;
    animation-play-state: paused;
  }

  .md-copy-btn.is-failed {
    border-color: #ef4444;
    background: #ef4444;
    color: #ffffff;
    animation-play-state: paused;
  }

  .md-copy-btn::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 0;
    height: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.45);
    opacity: 0;
    transform: translate(-50%, -50%);
    pointer-events: none;
  }

  .md-copy-btn.is-pressing::after {
    animation: md-theme-ripple 0.45s ease-out;
  }

  .md-copy-icon {
    position: relative;
    z-index: 1;
    display: grid;
    place-items: center;
  }

  .md-copy-label {
    display: none;
  }

  html[data-md-theme="dark"] .md-copy-btn {
    background: rgba(15, 23, 42, 0.96);
  }

  /* Pattern 6: section reveal with quiet depth */
  .md-reveal-section {
    opacity: 0;
    transform: translateY(28px) scale(0.992);
    transition: opacity 0.72s ease, transform 0.72s cubic-bezier(0.22, 1, 0.36, 1);
    will-change: opacity, transform;
  }

  .md-reveal-section.is-revealed {
    opacity: 1;
    transform: translateY(0) scale(1);
  }

  .md-reveal-section > h2:first-child {
    position: relative;
  }

  .md-reveal-section > h2:first-child::after {
    content: "";
    position: absolute;
    left: 0;
    bottom: -1px;
    width: 0;
    height: 2px;
    border-radius: 999px;
    background: var(--md-accent);
    transition: width 0.72s ease 0.16s;
  }

  .md-reveal-section.is-revealed > h2:first-child::after {
    width: min(180px, 44%);
  }

  @media (prefers-reduced-motion: reduce) {
    .md-reveal-section {
      opacity: 1;
      transform: none;
      transition: none;
    }

    .md-reveal-section > h2:first-child::after {
      display: none;
    }
  }

  /* Reading settings: shared controls */
  :root {
    --md-reader-font-size: 16px;
    --md-reader-code-font-size: 0.92rem;
    --md-reader-title-size: clamp(1.75rem, 3.4vw, 2.35rem);
    --md-reader-h1-size: 2rem;
    --md-reader-h2-size: 1.45rem;
    --md-reader-h3-size: 1.2rem;
    --md-reader-h4-size: 1.05rem;
    --md-reader-line-height: 1.9;
    --md-settings-panel-width: 340px;
    --md-settings-radius: 12px;
    --md-settings-offset-right: 66px;
    --md-settings-panel-bg: var(--md-panel-bg);
    --md-settings-shadow: 0 16px 38px rgba(15, 23, 42, 0.16);
    --md-settings-item-bg: var(--md-surface-soft);
    --md-settings-active-bg: var(--md-accent-soft);
    --md-settings-toggle-size: 44px;
    --md-icon-pulse-duration: 2.6s;
  }

  html :where(.md-article, .markdown-body, .content, .znc) {
    line-height: var(--md-reader-line-height) !important;
  }

  html :where(
    .md-article p,
    .md-article li,
    .md-article blockquote,
    .md-article td,
    .md-article th,
    .markdown-body p,
    .markdown-body li,
    .markdown-body blockquote,
    .markdown-body td,
    .markdown-body th,
    .content p,
    .content li,
    .content blockquote,
    .content td,
    .content th,
    .znc p,
    .znc li,
    .znc blockquote,
    .znc td,
    .znc th
  ) {
    font-size: var(--md-reader-font-size) !important;
  }

  html pre code {
    font-size: var(--md-reader-code-font-size) !important;
  }

  html :where(.md-file-title) {
    font-size: var(--md-reader-title-size) !important;
  }

  html :where(.md-article h1, .markdown-body h1, .content h1, .znc h1) {
    font-size: var(--md-reader-h1-size) !important;
  }

  html :where(.md-article h2, .markdown-body h2, .content h2, .znc h2) {
    font-size: var(--md-reader-h2-size) !important;
  }

  html :where(.md-article h3, .markdown-body h3, .content h3, .znc h3) {
    font-size: var(--md-reader-h3-size) !important;
  }

  html :where(.md-article h4, .markdown-body h4, .content h4, .znc h4) {
    font-size: var(--md-reader-h4-size) !important;
  }

  #md-header-actions {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    justify-content: flex-end;
    gap: 10px;
  }

  #md-pdf-export-toggle {
    position: relative;
    display: grid;
    place-items: center;
    width: var(--md-settings-toggle-size);
    height: var(--md-settings-toggle-size);
    border: 2px solid var(--md-accent);
    border-radius: 50%;
    background: #ffffff;
    color: var(--md-accent);
    cursor: pointer;
    overflow: hidden;
    opacity: 1;
    animation: md-theme-pulse var(--md-icon-pulse-duration) infinite;
    transition:
      transform 0.28s ease,
      box-shadow 0.28s ease,
      background-color 0.28s ease,
      color 0.28s ease,
      border-color 0.28s ease;
  }

  #md-pdf-export-toggle:hover,
  #md-pdf-export-toggle.is-exporting {
    background: var(--md-accent);
    color: #ffffff;
    box-shadow: 0 0 15px rgba(62, 168, 255, 0.62);
    transform: scale(1.06) rotate(360deg);
    animation-play-state: paused;
  }

  #md-pdf-export-toggle::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 0;
    height: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.45);
    opacity: 0;
    transform: translate(-50%, -50%);
    pointer-events: none;
  }

  #md-pdf-export-toggle.is-toggling::after {
    animation: md-theme-ripple 0.45s ease-out;
  }

  .md-pdf-export-icon {
    position: relative;
    z-index: 1;
    display: grid;
    place-items: center;
    width: 22px;
    height: 22px;
    transition: transform 0.28s ease;
  }

  .md-pdf-export-icon svg {
    position: relative;
    display: block;
    width: 21px;
    height: 21px;
  }

  #md-pdf-export-toggle:hover .md-pdf-export-icon,
  #md-pdf-export-toggle.is-exporting .md-pdf-export-icon {
    transform: none;
  }

  html[data-md-theme="dark"] #md-pdf-export-toggle {
    background: rgba(15, 23, 42, 0.96);
  }

  #md-reading-settings-toggle {
    position: relative;
    display: grid;
    place-items: center;
    width: var(--md-settings-toggle-size);
    height: var(--md-settings-toggle-size);
    border: 2px solid var(--md-accent);
    border-radius: 50%;
    background: #ffffff;
    color: var(--md-accent);
    cursor: pointer;
    overflow: hidden;
    opacity: 1;
    animation: md-theme-pulse var(--md-icon-pulse-duration) infinite;
    transition:
      transform 0.28s ease,
      box-shadow 0.28s ease,
      background-color 0.28s ease,
      color 0.28s ease,
      border-color 0.28s ease;
  }

  #md-reading-settings-toggle:hover,
  #md-reading-settings-toggle.is-open {
    background: var(--md-accent);
    color: #ffffff;
    box-shadow: 0 0 15px rgba(62, 168, 255, 0.62);
    transform: scale(1.06) rotate(90deg);
    animation-play-state: paused;
  }

  #md-reading-settings-toggle::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 0;
    height: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.45);
    opacity: 0;
    transform: translate(-50%, -50%);
    pointer-events: none;
  }

  #md-reading-settings-toggle.is-toggling::after {
    animation: md-theme-ripple 0.45s ease-out;
  }

  .md-reading-settings-icon {
    position: relative;
    z-index: 1;
    display: grid;
    place-items: center;
    width: 20px;
    height: 20px;
  }

  .md-reading-settings-icon svg {
    display: block;
    width: 20px;
    height: 20px;
  }

  html[data-md-theme="dark"] #md-reading-settings-toggle {
    background: rgba(15, 23, 42, 0.96);
  }

  #md-reading-settings-panel {
    position: fixed;
    top: calc(var(--md-fixed-header-height) + 10px);
    right: var(--md-settings-offset-right);
    z-index: 10002;
    box-sizing: border-box;
    width: min(var(--md-settings-panel-width), calc(100vw - 24px));
    max-height: calc(100vh - var(--md-fixed-header-height) - 24px);
    padding: 14px;
    border: 1px solid var(--md-border);
    border-radius: var(--md-settings-radius);
    background: var(--md-settings-panel-bg);
    color: var(--md-text);
    box-shadow: var(--md-settings-shadow);
    backdrop-filter: blur(14px);
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    overflow: auto;
    transform: translateY(-8px) scale(0.98);
    transform-origin: top right;
    transition:
      opacity 0.22s ease,
      visibility 0.22s ease,
      transform 0.22s ease;
  }

  #md-reading-settings-panel.is-open {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transform: translateY(0) scale(1);
  }

  .md-reading-settings-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 12px;
  }

  .md-reading-settings-title {
    color: var(--md-heading);
    font-size: 0.95rem;
    font-weight: 800;
    line-height: 1.2;
  }

  .md-reading-settings-group {
    padding: 12px 0;
    border-top: 1px solid var(--md-border);
  }

  .md-reading-settings-group:first-of-type {
    border-top: 0;
    padding-top: 2px;
  }

  .md-reading-settings-label {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 8px;
    color: var(--md-muted);
    font-size: 12px;
    font-weight: 800;
    line-height: 1.3;
  }

  .md-reading-settings-value {
    color: var(--md-accent);
    font-family: var(--md-font-latin-hero);
    font-size: 11px;
    font-weight: 800;
  }

  .md-reading-settings-options {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 6px;
  }

  .md-reading-settings-option,
  .md-reading-settings-toggle-option {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 34px;
    padding: 0 8px;
    border: 1px solid var(--md-border);
    border-radius: calc(var(--md-settings-radius) - 4px);
    background: var(--md-settings-item-bg);
    color: var(--md-text);
    cursor: pointer;
    font-size: 12px;
    font-weight: 800;
    line-height: 1;
    transition:
      transform 0.18s ease,
      box-shadow 0.18s ease,
      background-color 0.18s ease,
      color 0.18s ease,
      border-color 0.18s ease;
  }

  .md-reading-settings-option:hover,
  .md-reading-settings-toggle-option:hover {
    transform: translateY(-1px);
    border-color: var(--md-accent);
  }

  .md-reading-settings-option.is-active,
  .md-reading-settings-toggle-option.is-active {
    border-color: var(--md-accent);
    background: var(--md-settings-active-bg);
    color: var(--md-accent);
    box-shadow: inset 0 0 0 1px rgba(62, 168, 255, 0.18);
  }

  .md-reading-settings-toggle-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 8px;
  }

  .md-reading-settings-toggle-option {
    justify-content: space-between;
    min-height: 38px;
    line-height: 1.25;
  }

  .md-reading-settings-switch {
    position: relative;
    flex: 0 0 auto;
    width: 34px;
    height: 18px;
    border-radius: 999px;
    background: var(--md-border-strong);
    transition: background-color 0.18s ease;
  }

  .md-reading-settings-switch::after {
    content: "";
    position: absolute;
    top: 3px;
    left: 3px;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: #ffffff;
    box-shadow: 0 2px 6px rgba(15, 23, 42, 0.18);
    transition: transform 0.18s ease;
  }

  .md-reading-settings-toggle-option.is-active .md-reading-settings-switch {
    background: var(--md-accent);
  }

  .md-reading-settings-toggle-option.is-active .md-reading-settings-switch::after {
    transform: translateX(16px);
  }

  html.md-reader-hide-toc #md-toc {
    display: none !important;
  }

  @media (min-width: 1100px) {
    html.md-reader-hide-toc.md-toc-visible body {
      padding: var(--md-layout-top) var(--md-page-pad) var(--md-layout-bottom) var(--md-page-pad) !important;
    }

    html.md-reader-hide-toc.md-toc-visible :where([class*="Container_default__"]) {
      width: min(var(--md-content-max), calc(100vw - var(--md-page-pad) * 2)) !important;
      max-width: min(var(--md-content-max), calc(100vw - var(--md-page-pad) * 2)) !important;
    }
  }

  html.md-reader-motion-reduced,
  html.md-reader-motion-reduced * {
    scroll-behavior: auto !important;
  }

  html.md-reader-motion-reduced .md-reveal-section {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }

  html.md-reader-motion-reduced :where(
    #md-theme-toggle,
    #md-pdf-export-toggle,
    #md-reading-settings-toggle,
    #md-scroll-top,
    .md-copy-btn,
    .md-reading-settings-option,
    .md-reading-settings-toggle-option
  ) {
    animation: none !important;
    transition-duration: 0.01ms !important;
  }

  html.md-reader-motion-reduced :where(
    #md-theme-toggle,
    #md-pdf-export-toggle,
    #md-reading-settings-toggle,
    #md-scroll-top,
    .md-copy-btn
  )::after {
    animation: none !important;
  }

  html.md-reader-motion-reduced .md-pdf-export-icon {
    transition: none !important;
    transform: none !important;
  }

  html.md-pdf-rendering :where(
    #md-floating-controls,
    #md-reading-progress,
    #md-scroll-top,
    #md-toc,
    .md-copy-btn
  ) {
    display: none !important;
  }

  html.md-pdf-rendering .md-reveal-section {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }

  @media print {
    @page {
      size: A4;
      margin: 12mm;
    }

    :root {
      --md-bg: #ffffff;
      --md-surface: #ffffff;
      --md-surface-soft: #ffffff;
      --md-panel-bg: #ffffff;
    }

    * {
      -webkit-print-color-adjust: exact !important;
      print-color-adjust: exact !important;
    }

    html,
    body {
      width: auto !important;
      height: auto !important;
      min-height: 0 !important;
      max-height: none !important;
      overflow: visible !important;
    }

    body {
      background: #ffffff !important;
      padding: 0 !important;
    }

    #md-floating-controls,
    #md-reading-progress,
    #md-scroll-top,
    #md-toc,
    .md-copy-btn {
      display: none !important;
    }

    :where(
      #__next,
      main,
      article,
      section,
      .md-article,
      .markdown-body,
      .content,
      .znc,
      [class*="View_columnsContainer__"],
      [class*="View_content__"],
      [class*="View_main__"],
      [class*="Container_default__"]
    ) {
      box-sizing: border-box !important;
      width: auto !important;
      max-width: none !important;
      height: auto !important;
      min-height: 0 !important;
      max-height: none !important;
      overflow: visible !important;
      transform: none !important;
    }

    .md-article {
      background: #ffffff !important;
      margin: 0 !important;
      padding: 0 !important;
      border: 0 !important;
      box-shadow: none !important;
    }

    .md-reveal-section {
      display: block !important;
      opacity: 1 !important;
      transform: none !important;
      break-inside: auto !important;
      page-break-inside: auto !important;
    }

    :where(h1, h2, h3, h4) {
      break-after: avoid-page;
      page-break-after: avoid;
    }

    :where(pre, blockquote, table, tr, img, figure) {
      break-inside: avoid;
      page-break-inside: avoid;
    }
  }

  @media (max-width: 1099px) {
    #md-header-actions {
      gap: 8px;
    }

    #md-reading-settings-toggle {
      width: 40px;
      height: 40px;
    }

    #md-reading-settings-panel {
      top: calc(var(--md-fixed-header-height) + 8px);
      right: 12px;
      left: 12px;
      width: auto;
    }
  }
  /* Reading settings effect base: pattern 3 glass floating */
  :root {
    --md-settings-panel-width: 360px;
    --md-settings-radius: 16px;
    --md-settings-panel-bg: rgba(255, 255, 255, 0.88);
    --md-settings-shadow: 0 22px 52px rgba(15, 23, 42, 0.2);
    --md-settings-ui-accent: var(--md-accent);
    --md-settings-ui-ink: var(--md-text);
    --md-settings-ui-muted: var(--md-muted);
    --md-settings-ui-line: var(--md-border);
    --md-settings-ui-soft: transparent;
  }

  html[data-md-theme="dark"] {
    --md-settings-panel-bg: rgba(15, 23, 42, 0.86);
  }

  #md-reading-settings-panel {
    transform: translateY(-6px) scale(0.94);
  }

  #md-reading-settings-panel.is-open {
    transform: translateY(0) scale(1);
  }

  /* Reading settings base style 7: minimal underline tabs */
  :root {
    --md-settings-ui-accent: #475569;
    --md-settings-ui-soft: transparent;
    --md-settings-shadow: 0 18px 40px rgba(15, 23, 42, 0.13);
  }

  .md-reading-settings-options {
    gap: 0;
    border-bottom: 1px solid var(--md-border);
  }

  .md-reading-settings-option {
    border: 0;
    border-bottom: 3px solid transparent;
    border-radius: 0;
    background: transparent;
    color: var(--md-muted);
  }

  .md-reading-settings-option:hover {
    transform: none;
    color: var(--md-text);
  }

  .md-reading-settings-option.is-active {
    border-bottom-color: #3ea8ff;
    background: transparent;
    color: var(--md-text);
    box-shadow: none;
  }
  /* Toggle pattern 2: no-fill slim switches */
  .md-reading-settings-toggle-row {
    gap: 10px;
  }

  .md-reading-settings-toggle-option {
    min-height: 34px;
    padding: 0 2px;
    border: 0;
    border-radius: 0;
    background: transparent;
    color: var(--md-muted);
    box-shadow: none;
  }

  .md-reading-settings-toggle-option:hover {
    transform: none;
    color: var(--md-text);
  }

  .md-reading-settings-toggle-option.is-active {
    background: transparent;
    color: #0f83fd;
    box-shadow: none;
  }

  .md-reading-settings-switch {
    width: 32px;
    height: 18px;
    background: #d7dee8;
  }

  .md-reading-settings-switch::after {
    top: 3px;
    left: 3px;
    width: 12px;
    height: 12px;
  }

  .md-reading-settings-toggle-option.is-active .md-reading-settings-switch {
    background: #3ea8ff;
  }

  .md-reading-settings-toggle-option.is-active .md-reading-settings-switch::after {
    transform: translateX(14px);
  }
</style>

<script>
(() => {
  'use strict';

  const THEME_KEY = 'markdown-theme';
  const SETTINGS_KEY = 'markdown-reading-settings';
  const root = document.documentElement;
  const ARTICLE_SELECTOR = '.znc, .markdown-body, article, main, .content';
  const ARTICLE_CONTENT_SELECTOR =
    'h1, h2, h3, h4, h5, h6, p, ul, ol, blockquote, pre, table, hr, details, figure, img';

  const isEnhancerElement = (node) => {
    if (node.nodeType !== Node.ELEMENT_NODE) return false;

    const element = node;
    const ignoredTags = ['SCRIPT', 'STYLE', 'LINK', 'META', 'TEMPLATE', 'NOSCRIPT'];

    if (ignoredTags.includes(element.tagName)) return true;
    if (element.id && element.id.startsWith('md-')) return true;
    if (element.closest('#md-toc, #md-floating-controls, #md-reading-progress')) return true;

    return false;
  };

  const looksLikeArticle = (element) => {
    return Boolean(
      element &&
      element !== document.body &&
      !element.closest('#md-toc') &&
      element.querySelector(ARTICLE_CONTENT_SELECTOR)
    );
  };

  const prepareArticleRoot = () => {
    const existing = document.querySelector('.md-article');

    if (looksLikeArticle(existing)) {
      root.classList.add('md-article-ready');
      return existing;
    }

    const candidate = Array.from(document.querySelectorAll(ARTICLE_SELECTOR))
      .find(looksLikeArticle);

    if (candidate) {
      candidate.classList.add('md-article');
      root.classList.add('md-article-ready');
      return candidate;
    }

    const contentNodes = Array.from(document.body.childNodes).filter((node) => {
      if (node.nodeType === Node.TEXT_NODE) {
        return node.textContent.trim().length > 0;
      }

      if (node.nodeType !== Node.ELEMENT_NODE) return false;

      return !isEnhancerElement(node);
    });

    if (contentNodes.length === 0) return document.body;

    const article = document.createElement('main');
    article.id = 'md-article';
    article.className = 'md-article';

    document.body.insertBefore(article, contentNodes[0]);
    contentNodes.forEach((node) => article.appendChild(node));

    root.classList.add('md-plain-render', 'md-article-ready');
    return article;
  };

  const MOON_ICON = `
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <path
        d="M21 14.2A8.6 8.6 0 0 1 9.8 3a7.2 7.2 0 1 0 11.2 11.2Z"
        fill="currentColor"
      ></path>
    </svg>
  `;

  const SUN_ICON = `
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <path
        d="M12 18a6 6 0 1 0 0-12 6 6 0 0 0 0 12Zm0 4a1 1 0 0 0 1-1v-1a1 1 0 1 0-2 0v1a1 1 0 0 0 1 1Zm0-18a1 1 0 0 0 1-1V2a1 1 0 1 0-2 0v1a1 1 0 0 0 1 1Zm10 8a1 1 0 0 0-1-1h-1a1 1 0 1 0 0 2h1a1 1 0 0 0 1-1ZM4 12a1 1 0 0 0-1-1H2a1 1 0 1 0 0 2h1a1 1 0 0 0 1-1Zm15.1 7.1a1 1 0 0 0 0-1.4l-.7-.7A1 1 0 1 0 17 18.4l.7.7a1 1 0 0 0 1.4 0ZM7 7a1 1 0 0 0 0-1.4l-.7-.7A1 1 0 1 0 4.9 6.3l.7.7A1 1 0 0 0 7 7Zm12.1-2.1a1 1 0 0 0-1.4 0l-.7.7A1 1 0 1 0 18.4 7l.7-.7a1 1 0 0 0 0-1.4ZM7 17a1 1 0 0 0-1.4 0l-.7.7a1 1 0 1 0 1.4 1.4l.7-.7A1 1 0 0 0 7 17Z"
        fill="currentColor"
      ></path>
    </svg>
  `;

  const ARROW_UP_ICON = `
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <path
        d="M12 5.5 5.8 11.7a1 1 0 1 0 1.4 1.4L11 9.3V18a1 1 0 1 0 2 0V9.3l3.8 3.8a1 1 0 0 0 1.4-1.4L12 5.5Z"
        fill="currentColor"
      ></path>
    </svg>
  `;

  const COPY_ICON = `
    <svg viewBox="0 0 24 24" width="15" height="15" fill="none" aria-hidden="true">
      <path
        d="M8 7.5A2.5 2.5 0 0 1 10.5 5H18a2.5 2.5 0 0 1 2.5 2.5V15A2.5 2.5 0 0 1 18 17.5h-.5V18A2.5 2.5 0 0 1 15 20.5H7A2.5 2.5 0 0 1 4.5 18v-8A2.5 2.5 0 0 1 7 7.5h1Zm2 0V15c0 .28.22.5.5.5H18c.28 0 .5-.22.5-.5V7.5A.5.5 0 0 0 18 7h-7.5a.5.5 0 0 0-.5.5Zm-3.5 2.5v8c0 .28.22.5.5.5h8a.5.5 0 0 0 .5-.5v-.5h-5A2.5 2.5 0 0 1 8 15v-5H7a.5.5 0 0 0-.5.5Z"
        fill="currentColor"
      ></path>
    </svg>
  `;

  const CHECK_ICON = `
    <svg viewBox="0 0 24 24" width="15" height="15" fill="none" aria-hidden="true">
      <path
        d="M9.6 16.6 5.8 12.8a1 1 0 1 0-1.4 1.4l4.5 4.5a1 1 0 0 0 1.4 0l9.2-9.2a1 1 0 1 0-1.4-1.4l-8.5 8.5Z"
        fill="currentColor"
      ></path>
    </svg>
  `;

  const COPY_FAILED_ICON = `
    <svg viewBox="0 0 24 24" width="15" height="15" fill="none" aria-hidden="true">
      <path
        d="M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 4.5a1 1 0 0 1 1 1V12a1 1 0 1 1-2 0V8.5a1 1 0 0 1 1-1Zm0 8.9a1.15 1.15 0 1 1 0-2.3 1.15 1.15 0 0 1 0 2.3Z"
        fill="currentColor"
      ></path>
    </svg>
  `;

  const storage = {
    get(key) {
      try {
        return localStorage.getItem(key);
      } catch {
        return null;
      }
    },
    set(key, value) {
      try {
        localStorage.setItem(key, value);
      } catch {
        // localStorage が使えない環境では無視
      }
    }
  };

  const prefersDark = () => {
    return window.matchMedia &&
      window.matchMedia('(prefers-color-scheme: dark)').matches;
  };

  const getInitialTheme = () => {
    return storage.get(THEME_KEY) || (prefersDark() ? 'dark' : 'light');
  };

  const applyTheme = (theme) => {
    root.setAttribute('data-md-theme', theme);

    const button = document.getElementById('md-theme-toggle');
    if (!button) return;

    const isDark = theme === 'dark';
    button.innerHTML = `<span class="md-theme-toggle-icon">${isDark ? SUN_ICON : MOON_ICON}</span>`;
    button.classList.toggle('is-night-on', isDark);
    button.dataset.mode = isDark ? 'on' : 'off';
    button.setAttribute('aria-pressed', String(isDark));
    button.setAttribute(
      'aria-label',
      isDark ? 'ナイトモードをOFFにする' : 'ナイトモードをONにする'
    );
    button.title = isDark ? 'ナイトモードをOFFにする' : 'ナイトモードをONにする';
  };

  const fallbackCopy = (text) => {
    return new Promise((resolve, reject) => {
      const textarea = document.createElement('textarea');
      textarea.value = text;
      textarea.setAttribute('readonly', '');
      textarea.style.position = 'fixed';
      textarea.style.top = '-9999px';
      textarea.style.left = '-9999px';

      document.body.appendChild(textarea);
      textarea.select();

      try {
        const success = document.execCommand('copy');
        document.body.removeChild(textarea);
        success ? resolve() : reject(new Error('copy failed'));
      } catch (error) {
        document.body.removeChild(textarea);
        reject(error);
      }
    });
  };

  const copyText = (text) => {
    if (navigator.clipboard && window.isSecureContext) {
      return navigator.clipboard.writeText(text);
    }
    return fallbackCopy(text);
  };

  const isReaderMotionReduced = () => {
    return root.classList.contains('md-reader-motion-reduced');
  };

  const setCopyButtonContent = (button, icon, label, ariaLabel) => {
    button.innerHTML = `
      <span class="md-copy-icon">${icon}</span>
      <span class="md-copy-label">${label}</span>
    `;
    button.setAttribute('aria-label', ariaLabel);
    button.title = ariaLabel;
  };

  const enhanceCodeBlocks = (articleRoot = document) => {
    const codeBlocks = articleRoot.querySelectorAll('pre > code');

    codeBlocks.forEach((code) => {
      const pre = code.parentElement;
      if (!pre || pre.querySelector('.md-copy-btn')) return;

      const button = document.createElement('button');
      button.type = 'button';
      button.className = 'md-copy-btn';
      setCopyButtonContent(button, COPY_ICON, 'Copy', 'コードをコピー');

      button.addEventListener('click', async () => {
        button.classList.remove('is-copied', 'is-failed', 'is-pressing');

        if (!isReaderMotionReduced()) {
          void button.offsetWidth;
          button.classList.add('is-pressing');
          button.animate(
            [
              { transform: 'translateY(0) scale(1)' },
              { transform: 'translateY(-1px) scale(1.08)' },
              { transform: 'translateY(0) scale(1)' }
            ],
            { duration: 260, easing: 'ease-out' }
          );
          window.setTimeout(() => button.classList.remove('is-pressing'), 320);
        }

        try {
          await copyText(code.innerText);
          button.classList.add('is-copied');
          setCopyButtonContent(button, CHECK_ICON, 'Copied', 'コピーしました');
          setTimeout(() => {
            button.classList.remove('is-copied');
            setCopyButtonContent(button, COPY_ICON, 'Copy', 'コードをコピー');
          }, 1200);
        } catch {
          button.classList.add('is-failed');
          setCopyButtonContent(button, COPY_FAILED_ICON, 'Failed', 'コピーに失敗しました');
          setTimeout(() => {
            button.classList.remove('is-failed');
            setCopyButtonContent(button, COPY_ICON, 'Copy', 'コードをコピー');
          }, 1200);
        }
      });

      pre.appendChild(button);
    });
  };

  const GEAR_ICON = `
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <path
        d="M10.7 3.2a1.3 1.3 0 0 1 2.6 0l.2 1.5c.5.15.98.35 1.43.6l1.2-.92a1.3 1.3 0 0 1 1.8.13l1.56 1.56a1.3 1.3 0 0 1 .13 1.8l-.92 1.2c.25.45.45.93.6 1.43l1.5.2a1.3 1.3 0 0 1 0 2.6l-1.5.2c-.15.5-.35.98-.6 1.43l.92 1.2a1.3 1.3 0 0 1-.13 1.8l-1.56 1.56a1.3 1.3 0 0 1-1.8.13l-1.2-.92c-.45.25-.93.45-1.43.6l-.2 1.5a1.3 1.3 0 0 1-2.6 0l-.2-1.5a7.3 7.3 0 0 1-1.43-.6l-1.2.92a1.3 1.3 0 0 1-1.8-.13L4.51 17.93a1.3 1.3 0 0 1-.13-1.8l.92-1.2a7.3 7.3 0 0 1-.6-1.43l-1.5-.2a1.3 1.3 0 0 1 0-2.6l1.5-.2c.15-.5.35-.98.6-1.43l-.92-1.2a1.3 1.3 0 0 1 .13-1.8l1.56-1.56a1.3 1.3 0 0 1 1.8-.13l1.2.92c.45-.25.93-.45 1.43-.6l.2-1.5Zm1.3 5.25a3.55 3.55 0 1 0 0 7.1 3.55 3.55 0 0 0 0-7.1Z"
        fill="currentColor"
      ></path>
    </svg>
  `;

  const PRINTER_ICON = `
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <path
        d="M7 8V4.8C7 3.8 7.8 3 8.8 3h6.4c1 0 1.8.8 1.8 1.8V8h.7A3.3 3.3 0 0 1 21 11.3v4.4c0 .72-.58 1.3-1.3 1.3H17v2.2c0 1-.8 1.8-1.8 1.8H8.8c-1 0-1.8-.8-1.8-1.8V17H4.3c-.72 0-1.3-.58-1.3-1.3v-4.4A3.3 3.3 0 0 1 6.3 8H7Zm2-1h6V5H9v2Zm0 10v2h6v-2H9Zm8.8-3.2a1.1 1.1 0 1 0 0-2.2 1.1 1.1 0 0 0 0 2.2Z"
        fill="currentColor"
      ></path>
    </svg>
  `;

  const READING_SETTING_DEFAULTS = {
    font: 'normal',
    line: 'normal',
    motion: false,
    toc: true
  };

  const READING_SETTING_OPTIONS = {
    font: {
      compact: {
        label: '',
        value: '14px',
        code: '0.82rem',
        title: 'clamp(1.45rem, 2.7vw, 2rem)',
        h1: '1.65rem',
        h2: '1.2rem',
        h3: '1.02rem',
        h4: '0.94rem'
      },
      normal: {
        label: '標準',
        value: '16px',
        code: '0.92rem',
        title: 'clamp(1.75rem, 3.4vw, 2.35rem)',
        h1: '2rem',
        h2: '1.45rem',
        h3: '1.2rem',
        h4: '1.05rem'
      },
      large: {
        label: '',
        value: '18.5px',
        code: '1.04rem',
        title: 'clamp(2rem, 3.9vw, 2.75rem)',
        h1: '2.35rem',
        h2: '1.7rem',
        h3: '1.38rem',
        h4: '1.16rem'
      },
      xlarge: {
        label: '最大',
        value: '21px',
        code: '1.16rem',
        title: 'clamp(2.25rem, 4.5vw, 3.15rem)',
        h1: '2.7rem',
        h2: '1.98rem',
        h3: '1.58rem',
        h4: '1.28rem'
      }
    },
    line: {
      tight: { label: '詰め', value: '1.72' },
      normal: { label: '標準', value: '1.9' },
      relaxed: { label: '広め', value: '2.06' },
      loose: { label: '最大', value: '2.2' }
    }
  };

  const normalizeReadingSettings = (settings = {}) => {
    const { width, ...settingsWithoutWidth } = settings || {};
    const normalized = { ...READING_SETTING_DEFAULTS, ...settingsWithoutWidth };

    if (!READING_SETTING_OPTIONS.font[normalized.font]) normalized.font = READING_SETTING_DEFAULTS.font;
    if (!READING_SETTING_OPTIONS.line[normalized.line]) normalized.line = READING_SETTING_DEFAULTS.line;

    normalized.motion = Boolean(normalized.motion);
    normalized.toc = normalized.toc !== false;

    return normalized;
  };

  const getReadingSettings = () => {
    const stored = storage.get(SETTINGS_KEY);
    if (!stored) return { ...READING_SETTING_DEFAULTS };

    try {
      return normalizeReadingSettings(JSON.parse(stored));
    } catch {
      return { ...READING_SETTING_DEFAULTS };
    }
  };

  const saveReadingSettings = (settings) => {
    storage.set(SETTINGS_KEY, JSON.stringify(normalizeReadingSettings(settings)));
  };

  const renderReadingSettingsState = (settings = getReadingSettings()) => {
    const panel = document.getElementById('md-reading-settings-panel');
    if (!panel) return;

    const normalized = normalizeReadingSettings(settings);

    panel.querySelectorAll('[data-reader-setting]').forEach((button) => {
      const setting = button.dataset.readerSetting;
      const active = normalized[setting] === button.dataset.readerValue;
      button.classList.toggle('is-active', active);
      button.setAttribute('aria-pressed', String(active));
    });

    panel.querySelectorAll('[data-reader-value-label]').forEach((label) => {
      const setting = label.dataset.readerValueLabel;
      label.textContent = READING_SETTING_OPTIONS[setting]?.[normalized[setting]]?.label || '';
    });

    panel.querySelectorAll('[data-reader-toggle]').forEach((button) => {
      const setting = button.dataset.readerToggle;
      const active = Boolean(normalized[setting]);
      button.classList.toggle('is-active', active);
      button.setAttribute('aria-pressed', String(active));
      const state = button.querySelector('[data-reader-toggle-state]');
      if (state) state.textContent = active ? 'ON' : 'OFF';
    });
  };

  const applyReadingSettings = (settings = getReadingSettings()) => {
    const normalized = normalizeReadingSettings(settings);
    const font = READING_SETTING_OPTIONS.font[normalized.font];
    const line = READING_SETTING_OPTIONS.line[normalized.line];

    root.style.setProperty('--md-reader-font-size', font.value);
    root.style.setProperty('--md-reader-code-font-size', font.code);
    root.style.setProperty('--md-reader-title-size', font.title);
    root.style.setProperty('--md-reader-h1-size', font.h1);
    root.style.setProperty('--md-reader-h2-size', font.h2);
    root.style.setProperty('--md-reader-h3-size', font.h3);
    root.style.setProperty('--md-reader-h4-size', font.h4);
    root.style.setProperty('--md-reader-line-height', line.value);
    root.classList.toggle('md-reader-motion-reduced', normalized.motion);
    root.classList.toggle('md-reader-hide-toc', !normalized.toc);

    const toc = document.getElementById('md-toc');
    if (toc) toc.setAttribute('aria-hidden', String(!normalized.toc));

    renderReadingSettingsState(normalized);
  };

  const createSettingOptions = (setting) => {
    return Object.entries(READING_SETTING_OPTIONS[setting]).map(([value, option]) => `
      <button
        type="button"
        class="md-reading-settings-option"
        data-reader-setting="${setting}"
        data-reader-value="${value}"
        aria-pressed="false"
      >${option.label}</button>
    `).join('');
  };

  const setReadingSettingsPanelOpen = (open) => {
    const panel = document.getElementById('md-reading-settings-panel');
    const button = document.getElementById('md-reading-settings-toggle');
    if (!panel || !button) return;

    panel.classList.toggle('is-open', open);
    panel.setAttribute('aria-hidden', String(!open));
    button.classList.toggle('is-open', open);
    button.setAttribute('aria-expanded', String(open));
  };

  const createReadingSettingsButton = () => {
    const button = document.createElement('button');
    button.type = 'button';
    button.id = 'md-reading-settings-toggle';
    button.innerHTML = `<span class="md-reading-settings-icon">${GEAR_ICON}</span>`;
    button.setAttribute('aria-label', '設定を開く');
    button.setAttribute('aria-controls', 'md-reading-settings-panel');
    button.setAttribute('aria-expanded', 'false');
    button.title = '設定';

    const panel = document.createElement('section');
    panel.id = 'md-reading-settings-panel';
    panel.setAttribute('aria-label', '設定');
    panel.setAttribute('aria-hidden', 'true');
    panel.innerHTML = `
      <div class="md-reading-settings-head">
        <div class="md-reading-settings-title">設定</div>
      </div>
      <div class="md-reading-settings-group">
        <div class="md-reading-settings-label">文字サイズ <span class="md-reading-settings-value" data-reader-value-label="font"></span></div>
        <div class="md-reading-settings-options">${createSettingOptions('font')}</div>
      </div>
      <div class="md-reading-settings-group">
        <div class="md-reading-settings-label">行間 <span class="md-reading-settings-value" data-reader-value-label="line"></span></div>
        <div class="md-reading-settings-options">${createSettingOptions('line')}</div>
      </div>
      <div class="md-reading-settings-group">
        <div class="md-reading-settings-toggle-row">
          <button type="button" class="md-reading-settings-toggle-option" data-reader-toggle="motion" aria-pressed="false">
            <span>モーション控えめ</span>
            <span class="md-reading-settings-switch" aria-hidden="true"></span>
          </button>
          <button type="button" class="md-reading-settings-toggle-option" data-reader-toggle="toc" aria-pressed="true">
            <span>目次表示 <span data-reader-toggle-state>ON</span></span>
            <span class="md-reading-settings-switch" aria-hidden="true"></span>
          </button>
        </div>
      </div>
    `;

    panel.addEventListener('click', (event) => {
      const option = event.target.closest('[data-reader-setting]');
      const toggle = event.target.closest('[data-reader-toggle]');

      if (!option && !toggle) return;

      const next = getReadingSettings();

      if (option) {
        next[option.dataset.readerSetting] = option.dataset.readerValue;
      }

      if (toggle) {
        const key = toggle.dataset.readerToggle;
        next[key] = !next[key];
      }

      saveReadingSettings(next);
      applyReadingSettings(next);
    });

    button.addEventListener('click', () => {
      if (!isReaderMotionReduced()) {
        button.classList.remove('is-toggling');
        void button.offsetWidth;
        button.classList.add('is-toggling');
        window.setTimeout(() => button.classList.remove('is-toggling'), 450);
      }

      const panelIsOpen = document.getElementById('md-reading-settings-panel')?.classList.contains('is-open');
      setReadingSettingsPanelOpen(!panelIsOpen);
    });

    document.addEventListener('click', (event) => {
      if (!panel.classList.contains('is-open')) return;
      if (panel.contains(event.target) || button.contains(event.target)) return;
      setReadingSettingsPanelOpen(false);
    });

    document.addEventListener('keydown', (event) => {
      if (event.key === 'Escape') setReadingSettingsPanelOpen(false);
    });

    document.body.appendChild(panel);
    renderReadingSettingsState(getReadingSettings());

    return button;
  };

  const waitForPaint = () => {
    return new Promise((resolve) => {
      window.requestAnimationFrame(() => window.requestAnimationFrame(resolve));
    });
  };

  const printArticlePdf = async () => {
    const previousTheme = root.getAttribute('data-md-theme') || getInitialTheme();

    root.classList.add('md-pdf-rendering');
    applyTheme('light');

    try {
      setReadingSettingsPanelOpen(false);
      await document.fonts?.ready;
      await waitForPaint();
      window.print();
    } finally {
      window.setTimeout(() => {
        root.classList.remove('md-pdf-rendering');
        applyTheme(previousTheme);
      }, 600);
    }
  };

  const setPdfButtonState = (button, exporting) => {
    button.classList.toggle('is-exporting', exporting);
    button.setAttribute('aria-busy', String(exporting));
    button.disabled = exporting;
    button.title = exporting ? 'PDFを生成中' : 'PDF出力';
  };

  const createPdfExportButton = () => {
    const button = document.createElement('button');
    button.type = 'button';
    button.id = 'md-pdf-export-toggle';
    button.innerHTML = `
      <span class="md-pdf-export-icon">
        ${PRINTER_ICON}
      </span>
    `;
    button.setAttribute('aria-label', 'PDF出力');
    button.setAttribute('aria-busy', 'false');
    button.title = 'PDF出力';

    button.addEventListener('click', async () => {
      if (button.classList.contains('is-exporting')) return;

      if (!isReaderMotionReduced()) {
        button.classList.remove('is-toggling');
        void button.offsetWidth;
        button.classList.add('is-toggling');
        window.setTimeout(() => button.classList.remove('is-toggling'), 450);
      }

      setPdfButtonState(button, true);

      try {
        await printArticlePdf();
      } finally {
        window.setTimeout(() => setPdfButtonState(button, false), 300);
      }
    });

    return button;
  };

  const createThemeToggle = () => {
    if (document.getElementById('md-floating-controls')) return;

    const wrapper = document.createElement('div');
    wrapper.id = 'md-floating-controls';

    const button = document.createElement('button');
    button.type = 'button';
    button.id = 'md-theme-toggle';

    button.addEventListener('click', () => {
      const current = root.getAttribute('data-md-theme') || getInitialTheme();
      const next = current === 'dark' ? 'light' : 'dark';

      if (!isReaderMotionReduced()) {
        button.classList.remove('is-toggling');
        void button.offsetWidth;
        button.classList.add('is-toggling');
        window.setTimeout(() => button.classList.remove('is-toggling'), 450);
      }

      storage.set(THEME_KEY, next);
      applyTheme(next);
    });

    const actions = document.createElement('div');
    actions.id = 'md-header-actions';
    actions.appendChild(createPdfExportButton());
    actions.appendChild(createReadingSettingsButton());
    actions.appendChild(button);

    wrapper.appendChild(createHeaderTitle());
    wrapper.appendChild(actions);
    document.body.appendChild(wrapper);
  };

  const createReadingProgress = () => {
    if (document.getElementById('md-reading-progress')) return;

    const progress = document.createElement('div');
    progress.id = 'md-reading-progress';
    document.body.appendChild(progress);

    const update = () => {
      const scrollTop = window.scrollY || document.documentElement.scrollTop;
      const max =
        document.documentElement.scrollHeight -
        document.documentElement.clientHeight;

      const percent = max > 0 ? (scrollTop / max) * 100 : 0;
      progress.style.width = `${Math.min(100, Math.max(0, percent))}%`;
    };

    window.addEventListener('scroll', update, { passive: true });
    window.addEventListener('resize', update);
    update();
  };

  const createScrollTopButton = (articleRoot = document.querySelector('.md-article')) => {
    if (document.getElementById('md-scroll-top')) return;

    const button = document.createElement('button');
    button.type = 'button';
    button.id = 'md-scroll-top';
    button.innerHTML = `<span class="md-scroll-top-icon">${ARROW_UP_ICON}</span>`;
    button.setAttribute('aria-label', 'ページ上部へ戻る');
    button.setAttribute('aria-hidden', 'true');
    button.tabIndex = -1;
    button.title = 'ページ上部へ戻る';

    const updatePosition = () => {
      const target = articleRoot || document.querySelector('.md-article');
      const rect = target?.getBoundingClientRect();
      const size = button.offsetWidth || 44;
      const inset = 16;

      if (!rect || window.innerWidth < 1100) {
        button.style.left = '';
        button.style.right = '14px';
        button.style.bottom = '18px';
        return;
      }

      const left = Math.min(
        window.innerWidth - size - inset,
        Math.max(inset, rect.right - size - inset)
      );
      const bottom = Math.min(
        window.innerHeight - size - inset,
        Math.max(28, window.innerHeight - rect.bottom + inset)
      );

      button.style.left = `${left}px`;
      button.style.right = 'auto';
      button.style.bottom = `${bottom}px`;
    };

    const update = () => {
      const visible = (window.scrollY || document.documentElement.scrollTop) > 4;
      updatePosition();
      button.classList.toggle('is-visible', visible);
      button.setAttribute('aria-hidden', String(!visible));
      button.tabIndex = visible ? 0 : -1;
    };

    button.addEventListener('click', () => {
      if (!isReaderMotionReduced()) {
        button.classList.remove('is-toggling');
        void button.offsetWidth;
        button.classList.add('is-toggling');
        window.setTimeout(() => button.classList.remove('is-toggling'), 450);
      }

      window.scrollTo({
        top: 0,
        behavior: isReaderMotionReduced() ? 'auto' : 'smooth'
      });
    });

    window.addEventListener('scroll', update, { passive: true });
    window.addEventListener('resize', update);
    document.body.appendChild(button);
    update();
  };

  const getFileName = () => {
    let path = window.location?.pathname || '';

    try {
      path = decodeURIComponent(path);
    } catch {
      path = path.replace(/%20/g, ' ');
    }

    const file = path.split('/').pop() || '';
    return file.replace(/\.[^/.]+$/, '').trim();
  };

  const getHeaderTitleText = () => {
    return (getFileName() || document.title || '初めまして').trim();
  };

  const createArticleTitle = (articleRoot) => {
    if (!articleRoot || articleRoot.querySelector('.md-file-title')) return;

    const title = document.createElement('div');
    title.className = 'md-file-title';
    title.textContent = getHeaderTitleText();

    articleRoot.insertBefore(title, articleRoot.firstChild);
  };

  const createHeaderTitle = () => {
    const title = document.createElement('div');
    title.id = 'md-fixed-title';
    title.setAttribute('aria-label', '記事タイトル');

    const main = document.createElement('span');
    main.className = 'md-fixed-title-main';
    main.textContent = getHeaderTitleText();

    title.appendChild(main);
    return title;
  };

  const createToc = (articleRoot = prepareArticleRoot()) => {
    if (document.getElementById('md-toc')) return;

    const container = articleRoot || document.body;

    const headings = Array.from(
      container.querySelectorAll('h1, h2, h3, h4')
    ).filter((heading) => !heading.closest('#md-toc'));

    if (headings.length === 0) return;

    const nav = document.createElement('nav');
    nav.id = 'md-toc';
    nav.setAttribute('aria-label', '目次');

    const title = document.createElement('div');
    title.className = 'md-toc-title';
    title.textContent = '目次';

    const links = document.createElement('div');
    links.className = 'md-toc-links';

    headings.forEach((heading, index) => {
      if (!heading.id) {
        heading.id = 'md-heading-' + index;
      }

      const link = document.createElement('a');
      link.href = '#' + heading.id;
      link.textContent = heading.textContent.trim();
      link.dataset.level = heading.tagName.replace('H', '');
      link.dataset.headingId = heading.id;

      links.appendChild(link);
    });

    nav.appendChild(title);
    nav.appendChild(links);
    document.body.appendChild(nav);

    root.classList.add('md-toc-visible');

    observeActiveHeading(headings, links);
  };

  const observeActiveHeading = (headings, linksContainer) => {
    const links = Array.from(linksContainer.querySelectorAll('a'));
    const ACTIVE_OFFSET = 96;

    const setActive = (id) => {
      links.forEach((link) => {
        const active = link.dataset.headingId === id;
        link.classList.toggle('is-active', active);

        if (active) {
          link.setAttribute('aria-current', 'true');
        } else {
          link.removeAttribute('aria-current');
        }
      });
    };

    const getActiveHeadingId = () => {
      let activeId = headings[0]?.id;

      headings.forEach((heading) => {
        if (heading.getBoundingClientRect().top <= ACTIVE_OFFSET) {
          activeId = heading.id;
        }
      });

      return activeId;
    };

    let ticking = false;
    let lockedActiveId = null;
    let unlockTimer = null;

    const updateActive = () => {
      setActive(lockedActiveId || getActiveHeadingId());
    };

    const requestUpdate = () => {
      if (ticking) return;

      ticking = true;
      window.requestAnimationFrame(() => {
        ticking = false;
        updateActive();
      });
    };

    links.forEach((link) => {
      link.addEventListener('click', (event) => {
        const target = document.getElementById(link.dataset.headingId);
        if (!target) return;

        event.preventDefault();
        lockedActiveId = link.dataset.headingId;
        window.clearTimeout(unlockTimer);
        setActive(lockedActiveId);

        window.history.pushState(null, '', link.getAttribute('href'));
        window.scrollTo({
          top:
            target.getBoundingClientRect().top +
            window.scrollY -
            ACTIVE_OFFSET +
            4,
          behavior: isReaderMotionReduced() ? 'auto' : 'smooth'
        });

        unlockTimer = window.setTimeout(() => {
          lockedActiveId = null;
          requestUpdate();
        }, 700);
      });
    });

    window.addEventListener('scroll', requestUpdate, { passive: true });
    window.addEventListener('resize', requestUpdate);
    updateActive();
  };

  const createRevealSections = (articleRoot) => {
    const headings = Array.from(articleRoot.querySelectorAll('h2'));

    return headings.map((heading, index) => {
      const section = document.createElement('section');
      section.className = 'md-reveal-section';
      section.dataset.revealSection = String(index + 1);
      heading.parentNode.insertBefore(section, heading);

      let node = heading;
      while (node) {
        const next = node.nextSibling;
        section.appendChild(node);
        if (next?.nodeType === Node.ELEMENT_NODE && next.tagName === 'H2') break;
        node = next;
      }

      return section;
    });
  };

  const enhanceScrollReveal = (articleRoot) => {
    if (!articleRoot || root.classList.contains('md-reveal-ready')) return;

    const sections = createRevealSections(articleRoot);

    if (!('IntersectionObserver' in window)) {
      sections.forEach((section) => section.classList.add('is-revealed'));
      root.classList.add('md-reveal-ready');
      return;
    }

    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (!entry.isIntersecting) return;
        entry.target.classList.add('is-revealed');
        observer.unobserve(entry.target);
      });
    }, { rootMargin: '0px 0px -14% 0px', threshold: 0.1 });

    sections.forEach((section) => observer.observe(section));
    root.classList.add('md-reveal-ready');
  };

  const init = () => {
    const articleRoot = prepareArticleRoot();

    createArticleTitle(articleRoot);
    enhanceScrollReveal(articleRoot);
    applyReadingSettings(getReadingSettings());
    applyTheme(getInitialTheme());
    createThemeToggle();
    applyTheme(root.getAttribute('data-md-theme') || getInitialTheme());
    createReadingProgress();
    createScrollTopButton(articleRoot);
    enhanceCodeBlocks(articleRoot);
    createToc(articleRoot);
    applyReadingSettings(getReadingSettings());
  };

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();
</script>

補足:注意点

この仕組みは、Markdownビューアや変換ツールがHTMLタグを残してくれる前提です。
セキュリティ上、<script> を実行しないMarkdownビューアも普通にあります。その場合、CSSだけ効く、または何も効かないことがあります。

また、公開サービスに投稿する場合は、サービス側の規約やHTML制限に合わせる必要があります。この記事の仕組みは、まずは自分の手元でMarkdownを読みやすくするためのもの、と考えるのがよさそうです。

まとめ

生成AIでMarkdownを書く量が増えるほど、次に効いてくるのは「どう読むか」です。
Markdownは、AIにも人間にも扱いやすい良い形式です。ただ、読む量が増えたなら、読む画面も少し整えたほうがいい。今回の <style><script> は、そのための自分用ビューアです。

文章の中身はMarkdownに任せる。読み心地はHTML側で整える。
この分け方にしておくと、生成AI時代の大量Markdownとも、少し落ち着いて付き合いやすくなります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?