First CH Designs
PGE-09Pages

Long-form Legal Document Page

A page layout for long legal documents — terms of service, privacy policies, statutory notices — built purely for readability. Every navigation path (the sticky clause index, inline cross-references, and the revision log's affected-article links) resolves to the same jump target, and the destination is marked by a briefly overlaid tint rather than a colour transition. The article/paragraph/item hierarchy is expressed with hanging numerals and hairline rules instead of indentation, so line starts stay aligned at any width. Measure is set in em, so switching between the normal and large type sizes keeps roughly 46 Japanese characters per line. The revision log sits at the end in descending date order. No rounded corners, no decoration, no scroll reveals. Sample text is fictional. Zero images, zero dependencies.

Added:
2026-09-21
Dependencies:
None
tags
#page #legal #terms #privacy-policy #long-form #reading #index #scrollspy #cross-reference #hairline #a11y #no-image #no-deps

Preview

サービス共通の規程

利用規約

本ページは、当社サービスの利用条件を定めた規程の全文です。条文は全11条で、章ごとに区切っています。改定の履歴は末尾に掲載しています。

制定
2024.04.01
最終改定
2026.08.20
条数
11

1章 総則

2章 利用契約

3章 利用上の義務と責任

4章 雑則

附則

本規約は2026年8月20日から施行します。施行日より前に成立した利用契約については、なお従前の例によります。

改定履歴

  1. 2026.08.20改定

    支払手段の追加にともない、支払期限と遅延損害金の取扱いを号として整理しました。

    対象

  2. 2025.11.05改定

    用語の定義を4号に整理し、禁止事項に不正アクセスに関する号を追加しました。

    対象

  3. 2025.04.01改定

    提供物の権利移転の時点を、対価の支払い完了時と明記しました。

    対象

  4. 2024.04.01制定

    本規約を制定し、同日から施行しました。

本規約に関するお問い合わせは、お問い合わせフォームまたは記載の窓口までご連絡ください。

"use client";

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Montserrat } from "next/font/google";

const montserrat = Montserrat({
  weight: ["500", "600"],
  subsets: ["latin"],
  display: "swap",
});

/**
 * 規約・プライバシーポリシーの読み物レイアウト(条→項→号の番号組み)
 * 既存の「左に追従する見出し+右のリスト」型は工程5件程度の短い読み物で、定義リスト型の書類も
 * 1画面で読み切れる分量だった。本ブロックはその逆で、数千字の条文を最後まで読ませるための版面に
 * 振り切っている。移動の単位が節ではなく条番号であること(目次・本文中の相互参照・改定履歴の
 * 対象条項が、すべて同じ条へのジャンプで繋がる)、階層をインデントではなく罫線と番号組みで示すこと、
 * 行長と文字サイズを読み手が持てることが、密度・操作の両面での違い。
 * 飽和idiomは英字キッカーと末尾の「→」リンクを外し、角丸・出現アニメも使わない(書類は開いた
 * 瞬間に全文が読める状態に置く)。掲載文は架空のサンプル条文。画像素材ゼロ・依存ゼロ。
 *
 * 汎用技法メモ(web-design-playbook 還流用):
 * - **和文の行長は em で決める**: 1文字≒1em なので `max-w-[46em]` がそのまま46文字前後になる。
 *   px で決めると文字サイズを変えた瞬間に行長が崩れるが、em なら読み手が文字を大きくしても
 *   1行の文字数は変わらない(可読性の設計値が保たれる)。
 * - **文字サイズは1箇所の inline style + 子は em 相対**: 親に fontSize を置き、子の寸法を
 *   text-[0.82em] のように em で書くと、切替ボタン1つで版面全体が等比で伸縮する。
 *   子に px のクラスを散らすと切替が効かない要素がまだらに残る。
 * - **階層はインデントでなく「番号の桁揃え+罫線」**: 項は grid-cols-[2.4em_1fr] のぶら下げ番号、
 *   号は行ごとの上罫で仕切る。左端が全階層で揃うので、深い階層でも行頭が階段状に逃げず、
 *   狭幅で行長が削られない(インデント方式は375pxで号の行長が2割減る)。
 * - **スクロールスパイは IntersectionObserver より rect の総なめが素直**: 条が可変高で連続して
 *   いる文書では「上端が閾値を超えた最後の要素」が正解。rAF で束ねた scroll ハンドラで
 *   getBoundingClientRect を回し、読了率も同じ1パスで出す(監視対象の出入りで状態が飛ばない)。
 * - **ジャンプ先の目印は色の遷移でなく重ねた面の opacity**: 背景色そのものを transition すると
 *   reduced-motion で止めたとき色が戻らないことがある。absolute な面を -z-10 に敷いて
 *   opacity だけを動かせば、停止時は「即座に付いて即座に消える」で破綻しない。
 * - **アンカー位置は scroll-mt-* に任せる**: sticky ヘッダーの高さをJSで測って引き算しなくても、
 *   見出し側に scroll-mt を付ければ scrollIntoView も素の #ハッシュ遷移も同じ位置に止まる。
 */

type Seg = string | { ref: number };

type Para = {
  /** 項の本文({ ref: n } は第n条への相互参照になる) */
  body: Seg[];
  /** 号(各号は上罫で仕切る。インデントは足さない) */
  items?: string[];
};

type Clause = { no: number; title: string; paras: Para[] };

type Chapter = { no: number; title: string; clauses: Clause[] };

const chapters: Chapter[] = [
  {
    no: 1,
    title: "総則",
    clauses: [
      {
        no: 1,
        title: "適用",
        paras: [
          {
            body: [
              "本規約は、当社が提供するサービス(以下「本サービス」といいます)の利用条件を、当社と利用者との間で定めるものです。利用者は、本規約に同意したうえで本サービスを利用するものとします。",
            ],
          },
          {
            body: [
              "当社が本サービス上で別途掲示する個別規定・ガイドライン・注意書きは、本規約の一部を構成します。本規約の定めと個別規定の定めが異なる場合は、個別規定の定めが優先して適用されます。",
            ],
          },
          {
            body: [
              "本規約の一部が法令に照らして無効または執行不能と判断された場合でも、残りの条項の効力は影響を受けません。",
            ],
          },
        ],
      },
      {
        no: 2,
        title: "定義",
        paras: [
          {
            body: ["本規約において使用する用語の意義は、次の各号に定めるとおりとします。"],
            items: [
              "本サービス 当社が運営するウェブサイト上で提供する制作・保守に関する一連の役務をいいます。",
              "利用者 本規約に同意し、本サービスの提供を受ける個人または法人をいいます。",
              "登録情報 利用登録にあたって利用者が当社に届け出た氏名・連絡先その他の情報をいいます。",
              "提供物 本サービスの結果として当社が利用者に引き渡す成果物および付随する資料をいいます。",
            ],
          },
          {
            body: [
              "前項に定めのない用語は、取引の通念および本規約の各条の文脈に従って解釈するものとします。",
            ],
          },
        ],
      },
    ],
  },
  {
    no: 2,
    title: "利用契約",
    clauses: [
      {
        no: 3,
        title: "利用登録",
        paras: [
          {
            body: [
              "利用登録は、当社所定の様式により申込みを行い、当社がこれを承諾した時点で成立します。承諾の通知は、登録情報として届け出られた連絡先宛に行います。",
            ],
          },
          {
            body: ["当社は、次の各号のいずれかに該当する場合、利用登録の申込みを承諾しないことがあります。"],
            items: [
              "申込みの内容に虚偽の記載、誤記または記入漏れがあった場合",
              "過去に本規約に違反したことを理由として利用を停止された者からの申込みである場合",
              "その他、当社が利用登録を相当でないと合理的に判断した場合",
            ],
          },
        ],
      },
      {
        no: 4,
        title: "アカウントの管理",
        paras: [
          {
            body: [
              "利用者は、自己の責任において、本サービスのアカウントおよびパスワードを適切に管理するものとします。第三者への譲渡・貸与・共用はできません。",
            ],
          },
          {
            body: [
              "登録情報に変更が生じた場合、利用者は遅滞なく当社所定の方法で変更を届け出るものとします。届出がないことにより通知が到達しなかった場合、当社は",
              { ref: 3 },
              "に定める連絡先への発信をもって通知が到達したものとみなします。",
            ],
          },
        ],
      },
      {
        no: 5,
        title: "料金および支払方法",
        paras: [
          {
            body: [
              "利用者は、本サービスの対価として、当社が別途提示する見積書または料金表に定める金額を、当社が指定する方法で支払うものとします。",
            ],
          },
          {
            body: ["支払いに関する取扱いは、次の各号のとおりとします。"],
            items: [
              "支払期限は、当社が発行する請求書に記載された日とします。",
              "振込手数料その他の支払いに要する費用は、利用者の負担とします。",
              "支払いが遅延した場合、利用者は年14.6パーセントの割合による遅延損害金を支払うものとします。",
            ],
          },
          {
            body: [
              "いったん支払われた対価は、法令に定めがある場合を除き返還しません。役務の提供が当社の責めに帰すべき事由により行われなかった場合は、この限りではありません。",
            ],
          },
        ],
      },
    ],
  },
  {
    no: 3,
    title: "利用上の義務と責任",
    clauses: [
      {
        no: 6,
        title: "禁止事項",
        paras: [
          {
            body: ["利用者は、本サービスの利用にあたり、次の各号に掲げる行為をしてはなりません。"],
            items: [
              "法令または公序良俗に違反する行為",
              "当社、他の利用者または第三者の権利・利益を侵害する行為",
              "本サービスの運営を妨げ、またはその信用を毀損する行為",
              "本サービスにより得られた情報を、当社の書面による事前の承諾なく商業的に利用する行為",
              "不正アクセスその他これに類する行為",
            ],
          },
          {
            body: [
              "当社は、利用者の行為が前項各号に該当すると判断した場合、事前の通知なく当該行為に関する情報の削除その他必要な措置を講じることができます。",
            ],
          },
        ],
      },
      {
        no: 7,
        title: "本サービスの停止・中断",
        paras: [
          {
            body: [
              "当社は、設備の保守点検、天災その他の不可抗力、または当社が必要と判断した事由により、利用者への事前の通知なく本サービスの全部または一部の提供を停止し、または中断することができます。",
            ],
          },
          {
            body: [
              "前項の措置により利用者に生じた損害について、当社は",
              { ref: 9 },
              "に定める範囲を超えて責任を負いません。",
            ],
          },
        ],
      },
      {
        no: 8,
        title: "知的財産権",
        paras: [
          {
            body: [
              "提供物に関する著作権その他の知的財産権は、対価の支払いが完了した時点で、当社に留保される部分を除き利用者に移転します。",
            ],
          },
          {
            body: [
              "前項の規定にかかわらず、当社が従前から保有していた技術・手法・汎用的な部品に関する権利は、当社に留保されます。利用者は、提供物の利用に必要な範囲でこれらを使用できます。",
            ],
          },
        ],
      },
      {
        no: 9,
        title: "免責事項",
        paras: [
          {
            body: [
              "当社は、本サービスが利用者の特定の目的に適合すること、期待する成果を生じること、および不具合が生じないことを保証しません。",
            ],
          },
          {
            body: [
              "当社の責めに帰すべき事由により利用者に損害が生じた場合、当社が負う賠償の範囲は、現実に生じた通常の損害に限られ、その上限は当該損害の発生した月に利用者から受領した対価の額とします。",
            ],
          },
        ],
      },
    ],
  },
  {
    no: 4,
    title: "雑則",
    clauses: [
      {
        no: 10,
        title: "本規約の変更",
        paras: [
          {
            body: [
              "当社は、法令の定めに従い、必要と判断した場合に本規約を変更することができます。変更後の本規約は、本ページに掲示した時点から効力を生じます。",
            ],
          },
          {
            body: [
              "利用者に重大な影響を及ぼす変更を行う場合、当社は効力発生日の30日前までに、変更内容と効力発生日を本ページおよび登録情報の連絡先宛に周知します。変更の履歴は末尾の改定履歴に掲載します。",
            ],
          },
        ],
      },
      {
        no: 11,
        title: "準拠法および管轄",
        paras: [
          {
            body: [
              "本規約の解釈および適用は、日本法に準拠します。本サービスに関して当社と利用者との間に紛争が生じた場合、当社の本店所在地を管轄する地方裁判所を第一審の専属的合意管轄裁判所とします。",
            ],
          },
          {
            body: [
              "本規約に定めのない事項、または各条の解釈に疑義が生じた事項については、当社と利用者が誠実に協議のうえ解決を図るものとします。",
            ],
          },
        ],
      },
    ],
  },
];

/** 改定履歴(日付降順。対象条項は本文と同じジャンプで繋がる) */
const revisions: { date: string; label: string; summary: string; refs: number[] }[] = [
  {
    date: "2026.08.20",
    label: "改定",
    summary: "支払手段の追加にともない、支払期限と遅延損害金の取扱いを号として整理しました。",
    refs: [5],
  },
  {
    date: "2025.11.05",
    label: "改定",
    summary: "用語の定義を4号に整理し、禁止事項に不正アクセスに関する号を追加しました。",
    refs: [2, 6],
  },
  {
    date: "2025.04.01",
    label: "改定",
    summary: "提供物の権利移転の時点を、対価の支払い完了時と明記しました。",
    refs: [8],
  },
  { date: "2024.04.01", label: "制定", summary: "本規約を制定し、同日から施行しました。", refs: [] },
];

const clauseId = (no: number) => `legal-clause-${no}`;

const flatClauses = chapters.flatMap((chapter) => chapter.clauses);

export default function LegalLongformPage() {
  const [activeClause, setActiveClause] = useState<number>(flatClauses[0]!.no);
  const [progress, setProgress] = useState(0);
  const [large, setLarge] = useState(false);
  const [flashed, setFlashed] = useState<number | null>(null);
  const articleRef = useRef<HTMLDivElement>(null);
  const flashTimer = useRef<ReturnType<typeof setTimeout> | null>(null);

  // スクロール位置から「いま読んでいる条」と読了率を1パスで出す(rAFで束ねる)
  useEffect(() => {
    let rafId = 0;
    let queued = false;

    const measure = () => {
      queued = false;
      const article = articleRef.current;
      if (!article) return;

      const threshold = window.innerHeight * 0.28;
      let current = flatClauses[0]!.no;
      for (const clause of flatClauses) {
        const el = document.getElementById(clauseId(clause.no));
        if (!el) continue;
        if (el.getBoundingClientRect().top <= threshold) current = clause.no;
      }
      setActiveClause(current);

      const rect = article.getBoundingClientRect();
      const read = -rect.top + window.innerHeight * 0.6;
      setProgress(Math.min(1, Math.max(0, rect.height > 0 ? read / rect.height : 0)));
    };

    const onScroll = () => {
      if (queued) return;
      queued = true;
      rafId = requestAnimationFrame(measure);
    };

    measure();
    window.addEventListener("scroll", onScroll, { passive: true });
    window.addEventListener("resize", onScroll, { passive: true });
    return () => {
      window.removeEventListener("scroll", onScroll);
      window.removeEventListener("resize", onScroll);
      cancelAnimationFrame(rafId);
    };
  }, []);

  useEffect(() => {
    return () => {
      if (flashTimer.current) clearTimeout(flashTimer.current);
    };
  }, []);

  // 目次・相互参照・改定履歴の3経路は、すべてこの1つのジャンプに集約する
  const jumpTo = useCallback((no: number) => {
    const el = document.getElementById(clauseId(no));
    if (!el) return;
    const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    el.scrollIntoView({ behavior: reduced ? "auto" : "smooth", block: "start" });
    el.focus({ preventScroll: true });
    setFlashed(no);
    if (flashTimer.current) clearTimeout(flashTimer.current);
    flashTimer.current = setTimeout(() => setFlashed(null), 1600);
  }, []);

  const titleOf = useMemo(() => {
    const map = new Map<number, string>();
    for (const clause of flatClauses) map.set(clause.no, clause.title);
    return map;
  }, []);

  const renderBody = (body: Seg[]) =>
    body.map((seg, i) =>
      typeof seg === "string" ? (
        <span key={i}>{seg}</span>
      ) : (
        <button
          key={i}
          type="button"
          onClick={() => jumpTo(seg.ref)}
          className="mx-[0.1em] border-b border-amber-600/50 font-semibold text-amber-700 transition-colors duration-150 hover:border-amber-700 hover:bg-amber-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600 motion-reduce:transition-none"
        >
          第{seg.ref}条({titleOf.get(seg.ref)})
        </button>
      ),
    );

  return (
    <section className="@container w-full max-w-5xl bg-white px-1 text-gray-900">
      {/* 文書ヘッダー */}
      <header className="border-b border-gray-900 pb-6">
        <p className="text-[11px] font-semibold text-gray-500">サービス共通の規程</p>
        <h2 className="mt-2 text-[clamp(1.6rem,3.6cqw,2.2rem)] leading-[1.35] font-bold tracking-tight">
          利用規約
        </h2>
        <div className="mt-4 flex flex-col gap-y-3 @2xl:flex-row @2xl:items-end @2xl:justify-between @2xl:gap-x-10">
          <p className="max-w-[34em] text-[13px] leading-[1.95] font-medium text-gray-600">
            本ページは、当社サービスの利用条件を定めた規程の全文です。条文は全11条で、章ごとに区切っています。改定の履歴は末尾に掲載しています。
          </p>
          <dl
            className={`${montserrat.className} flex shrink-0 gap-x-6 text-[11px] tabular-nums text-gray-500`}
          >
            <div>
              <dt className="text-gray-400">制定</dt>
              <dd className="mt-0.5 font-semibold text-gray-700">2024.04.01</dd>
            </div>
            <div>
              <dt className="text-gray-400">最終改定</dt>
              <dd className="mt-0.5 font-semibold text-gray-700">2026.08.20</dd>
            </div>
            <div>
              <dt className="text-gray-400">条数</dt>
              <dd className="mt-0.5 font-semibold text-gray-700">11</dd>
            </div>
          </dl>
        </div>
      </header>

      <div className="grid gap-y-10 @3xl:grid-cols-[12rem_1fr] @3xl:gap-x-10 @5xl:grid-cols-[14.5rem_1fr] @5xl:gap-x-14">
        {/* 条番号インデックス(広幅では読み進みに追従する) */}
        <nav
          aria-label="条文の目次"
          className="pt-8 @3xl:sticky @3xl:top-6 @3xl:self-start @3xl:pt-10"
        >
          <div className="flex items-baseline justify-between gap-x-3">
            <p className="text-[11px] font-semibold text-gray-500">目次</p>
            <p className={`${montserrat.className} text-[11px] tabular-nums text-gray-400`}>
              <span className="sr-only">読み進み </span>
              {Math.round(progress * 100)}%
            </p>
          </div>
          <div className="mt-2 h-px w-full bg-gray-200" aria-hidden="true">
            <div
              className="h-px w-full origin-left bg-amber-600 transition-transform duration-200 ease-out motion-reduce:transition-none"
              style={{ transform: `scaleX(${progress})` }}
            />
          </div>

          <ol className="mt-5 flex flex-col gap-y-5">
            {chapters.map((chapter) => (
              <li key={chapter.no}>
                <p className="text-[11px] leading-[1.7] font-semibold text-gray-400">
                  第{chapter.no}章 {chapter.title}
                </p>
                <ol className="mt-1.5 border-t border-gray-200">
                  {chapter.clauses.map((clause) => {
                    const active = activeClause === clause.no;
                    return (
                      <li key={clause.no} className="border-b border-gray-200">
                        <a
                          href={`#${clauseId(clause.no)}`}
                          onClick={(e) => {
                            e.preventDefault();
                            jumpTo(clause.no);
                          }}
                          aria-current={active ? "true" : undefined}
                          className="group relative flex items-baseline gap-x-2 py-2 pr-1 pl-3 transition-colors duration-200 hover:bg-amber-50/70 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-amber-600 motion-reduce:transition-none"
                        >
                          <span
                            aria-hidden="true"
                            className={`absolute inset-y-0 left-0 w-[2px] origin-center bg-amber-600 transition-transform duration-300 ease-out motion-reduce:transition-none ${
                              active ? "scale-y-100" : "scale-y-0"
                            }`}
                          />
                          <span
                            className={`${montserrat.className} shrink-0 text-[11px] tabular-nums ${
                              active ? "text-amber-700" : "text-gray-400"
                            }`}
                          >
                            {String(clause.no).padStart(2, "0")}
                          </span>
                          <span
                            className={`text-[12.5px] leading-[1.6] ${
                              active ? "font-bold text-gray-900" : "font-medium text-gray-600"
                            }`}
                          >
                            {clause.title}
                          </span>
                        </a>
                      </li>
                    );
                  })}
                </ol>
              </li>
            ))}
          </ol>

          {/* 文字サイズ: 行長は em で決めてあるので、切り替えても1行の文字数は変わらない */}
          <div className="mt-6 flex items-center gap-x-2 border-t border-gray-200 pt-4">
            <span className="text-[11px] font-semibold text-gray-500">文字サイズ</span>
            {[
              { key: false, label: "標準" },
              { key: true, label: "大" },
            ].map((opt) => (
              <button
                key={opt.label}
                type="button"
                aria-pressed={large === opt.key}
                onClick={() => setLarge(opt.key)}
                className={`border px-2.5 py-1 text-[11px] font-semibold transition-colors duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600 motion-reduce:transition-none ${
                  large === opt.key
                    ? "border-gray-900 bg-gray-900 text-white"
                    : "border-gray-300 text-gray-600 hover:border-gray-500"
                }`}
              >
                {opt.label}
              </button>
            ))}
          </div>
        </nav>

        {/* 本文: 行長は 46em ≒ 和文46文字で固定する */}
        <div
          ref={articleRef}
          style={{ fontSize: large ? "16.5px" : "15px" }}
          className="@3xl:pt-10"
        >
          {chapters.map((chapter) => (
            <section key={chapter.no} className="mt-10 first:mt-0">
              <h3 className="border-t-2 border-gray-900 pt-3 text-[0.87em] leading-[1.7] font-bold tracking-tight">
                第{chapter.no}章 {chapter.title}
              </h3>

              {chapter.clauses.map((clause) => (
                <article
                  key={clause.no}
                  id={clauseId(clause.no)}
                  tabIndex={-1}
                  className="relative isolate scroll-mt-20 border-t border-gray-200 pt-6 pb-7 first:border-t-0 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-amber-600"
                >
                  <span
                    aria-hidden="true"
                    className={`pointer-events-none absolute -inset-x-3 -inset-y-1 -z-10 bg-amber-50 transition-opacity duration-500 ease-out motion-reduce:transition-none ${
                      flashed === clause.no ? "opacity-100" : "opacity-0"
                    }`}
                  />
                  <h4 className="grid grid-cols-[3.6em_1fr] gap-x-1.5 text-[0.95em] leading-[1.7] font-bold">
                    <span className={`${montserrat.className} tabular-nums text-amber-700`}>
                      第{clause.no}条
                    </span>
                    <span>({clause.title})</span>
                  </h4>

                  <ol className="mt-3">
                    {clause.paras.map((para, pi) => (
                      <li
                        key={pi}
                        className="grid grid-cols-[1.8em_1fr] gap-x-2 pt-3 first:pt-0 last:pb-0 @xl:grid-cols-[2.4em_1fr]"
                      >
                        <span
                          className={`${montserrat.className} pt-[0.35em] text-[0.78em] tabular-nums text-gray-400`}
                          aria-hidden="true"
                        >
                          {clause.paras.length > 1 ? pi + 1 : ""}
                        </span>
                        <div className="max-w-[46em]">
                          <p className="text-[1em] leading-[2.05] text-gray-800">
                            {renderBody(para.body)}
                          </p>
                          {para.items && (
                            <ol className="mt-3 border-b border-gray-100">
                              {para.items.map((item, ii) => (
                                <li
                                  key={ii}
                                  className="grid grid-cols-[2.1em_1fr] gap-x-2 border-t border-gray-100 py-2 @xl:grid-cols-[2.6em_1fr]"
                                >
                                  <span
                                    className={`${montserrat.className} pt-[0.45em] text-[0.74em] tabular-nums text-gray-400`}
                                    aria-hidden="true"
                                  >
                                    ({ii + 1})
                                  </span>
                                  <span className="text-[0.94em] leading-[1.95] text-gray-700">
                                    {item}
                                  </span>
                                </li>
                              ))}
                            </ol>
                          )}
                        </div>
                      </li>
                    ))}
                  </ol>
                </article>
              ))}
            </section>
          ))}

          {/* 附則 */}
          <div className="mt-10 border-t-2 border-gray-900 pt-3">
            <h3 className="text-[0.87em] leading-[1.7] font-bold tracking-tight">附則</h3>
            <p className="mt-3 max-w-[46em] text-[0.94em] leading-[2.05] text-gray-700">
              本規約は2026年8月20日から施行します。施行日より前に成立した利用契約については、なお従前の例によります。
            </p>
          </div>

          {/* 改定履歴: 日付降順。対象条項は本文と同じジャンプで繋がる */}
          <div className="mt-10 border-t-2 border-gray-900 pt-3">
            <h3 className="text-[0.87em] leading-[1.7] font-bold tracking-tight">改定履歴</h3>
            <ol className="mt-4 border-b border-gray-200">
              {revisions.map((rev) => (
                <li
                  key={rev.date}
                  className="grid gap-y-1.5 border-t border-gray-200 py-4 @xl:grid-cols-[7.5em_4.5em_1fr] @xl:gap-x-4"
                >
                  <span
                    className={`${montserrat.className} text-[0.8em] leading-[1.9] font-semibold tabular-nums text-gray-900`}
                  >
                    {rev.date}
                  </span>
                  <span className="text-[0.75em] leading-[2.1] font-semibold text-gray-500">
                    {rev.label}
                  </span>
                  <div className="max-w-[40em]">
                    <p className="text-[0.88em] leading-[1.95] text-gray-700">{rev.summary}</p>
                    {rev.refs.length > 0 && (
                      <p className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1">
                        <span className="text-[0.72em] font-semibold text-gray-400">対象</span>
                        {rev.refs.map((no) => (
                          <button
                            key={no}
                            type="button"
                            onClick={() => jumpTo(no)}
                            className={`${montserrat.className} border-b border-amber-600/50 text-[0.76em] font-semibold tabular-nums text-amber-700 transition-colors duration-150 hover:border-amber-700 hover:bg-amber-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600 motion-reduce:transition-none`}
                          >
                            第{no}条
                          </button>
                        ))}
                      </p>
                    )}
                  </div>
                </li>
              ))}
            </ol>
            <p className="mt-5 text-[0.8em] leading-[1.95] text-gray-500">
              本規約に関するお問い合わせは、お問い合わせフォームまたは記載の窓口までご連絡ください。
            </p>
          </div>
        </div>
      </div>
    </section>
  );
}

Add to your project via shadcn CLI

npx shadcn@latest add https://designs.first-ch.com/r/legal-longform-page.json