跳转至

报纸版 HTML 设计口味

给「审核大纲、项目复盘、内部简报」等需要严肃感的页面做的中文报纸观感模板: 暖米纸面、粗黑分割线、宋体大标题、强对比、可打印。 靠排版建立层级,不用装饰图片、渐变、阴影、圆角。

效果演示

下面是用这套设计系统拼的一个迷你「文章索引」页,把报头、栏目条、按钮、徽标、信息卡、社论摘句、空状态、三个 JS 组件(自定义下拉、复制链接、加载遮罩)串成一个活例:

试这几下:

  • 顶部下拉切换类目 —— 「改值即整页提交」,加载遮罩短暂出现,筛选状态写进 URL 的 ?cat=;列表按类目过滤,切到没有文章的类目会看到空状态卡
  • 复制本页链接 —— 复制的是当前筛选后的完整 URL,toast 提示「链接已复制」
  • 对比按钮(默认 / 实心 / 禁用)、徽标、信息卡、摘句(普通 / 反色)几种排版语言

真身在 assets/newspaper-demo.html,样式与交互逻辑复用同级 newspaper.css / newspaper.js(同一份源文件,不重复定义,跟下面「接入方式」一致)。

何时用 / 何时不用

  • :网页文档、审核稿、长页、报告列表 / 门户、需要打印或导出 PDF 的严肃页面
  • 不用:演示页 / 滚动动画 / 投屏页面(那是另一套 16:9 舞台式风格)

接入方式

把本 skill assets/ 下三个文件(css / js / favicon)拷到你的项目 static/

  1. <head> 引入:

    <script>document.documentElement.className += ' js';</script>  <!-- 尽早执行,防原生下拉闪现 -->
    <link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
    <link rel="stylesheet" href="/static/css/newspaper.css">
    
  2. </body> 前(仅当用到 JS 组件时):

    <script src="/static/js/newspaper.js"></script>
    
  3. 多页项目建议做一个 Jinja base.html 放公共头/尾,各页 {% extends %} + {% block content %}

设计系统在 newspaper.css

设计令牌、字体、字重、分割线规则、现成类(.page / .masthead / .kicker / .section-rule / .part / .core-left / .btn / .badge / .card / .no-data 等)的完整定义和注释,都在 newspaper.css,按章节注释组织(---- 设计令牌 ---- / ---- 报头 ---- / ---- 区块 ---- 等)。直接读 CSS 比读 markdown 描述更准。

下面是核心骨架示例,更多见 CSS:

<div class="page">
  <header class="masthead">
    <div class="meta"><span>栏目 · KICKER</span><span>右侧元信息</span></div>
    <h1>大标题</h1>
    <p class="subtitle">副标题</p>
  </header>
  <div class="kicker">栏目条</div>
  <!-- 内容 -->
</div>

三个 JS 组件(需引 newspaper.js

全部 class 钩子驱动、无硬编码 id、有无 JS 兜底。

1. 自定义下拉

给原生 <select>class="np-select":JS 隐藏它、生成 .custom-select 列表(原生 select 仍作数据源 + 无 JS 兜底)。

「改值即整页提交」:把 select 放进 <form data-np-autosubmit>,加无 JS 兜底按钮 <noscript><button type="submit" class="btn">应用</button></noscript>

<form method="get" data-np-autosubmit>
  <select name="dept" class="np-select"><option value="">全部</option></select>
  <noscript><button type="submit" class="btn">应用</button></noscript>
</form>

2. 复制链接 + toast

<button class="np-copy" data-link="/view/foo">复制链接</button>

data-link 为相对路径时自动补 window.location.origin;绝对 URL 原样复制。 页面有 .toast 则复用,否则自动创建。

3. 加载遮罩

<div class="np-loading-region">
  <div class="loading-overlay">
    <div class="loading-box"><div class="label">正在加载…</div><div class="loading-bar"></div></div>
  </div>
  <!-- 结果内容 -->
</div>

任意表单 submit 时自动 .showpageshow(后退)时自动清除。

favicon

assets/favicon.svg 是极简圆框 + 两字母大字(默认 BI)+ 蓝图辅助网格的模板标记。 要换团队标识,改 <text> 里的字母即可(文件头注释里有示例)。

配套资产源码(折叠)

源文件全部在同级 assets/,下面通过 snippets 引用展示(源文件只一份,引用不算重复;真身也可按文件 URL 直接下载):

newspaper.css(含完整章节注释 —— 详细设计令牌、组件类、注释说明都在这)
/* ============================================================
   报纸版设计系统 (newspaper-style)
   暖米纸面 · 粗黑分割线 · 宋体大标题 · 强对比 · 可打印
   用法:<link rel="stylesheet" href=".../newspaper.css">
   配合 newspaper.js 可启用自定义下拉 / 复制链接 / 加载遮罩组件。
   ============================================================ */

/* ---- 设计令牌 ---- */
:root {
    --paper: #f3efe4;  /* 暖米色纸张背景 */
    --ink: #151515;    /* 主文字与粗线 */
    --muted: #595247;  /* 报头元信息、栏目 kicker */
    --body: #2e2a24;   /* 正文色,比纯黑柔和 */
    --accent: #8a2f1b; /* 强调色(章节编号 / 少量重点) */
}

/* ---- 基础 ---- */
* { box-sizing: border-box; }

html,
body {
    min-height: 100vh;
    margin: 0;
    background: var(--paper);
    color: var(--ink);
    font-family: "Songti SC", "Noto Serif CJK SC", "STSong", Georgia, serif;
}

/* 列向 flex,配合 .page 的四向 auto margin 实现版心上下居中 */
body {
    display: flex;
    flex-direction: column;
}

/* ---- 版心 ---- */
.page {
    width: min(1120px, calc(100% - 40px));
    /* 四向 auto:内容比视口短时上下左右居中,超高时也不裁切顶部 */
    margin: auto;
    padding: 24px 0 36px;
}

/* ---- 报头 ---- */
.masthead {
    border-top: 4px solid var(--ink);
    border-bottom: 4px solid var(--ink);
    padding: 22px 0 24px;
}

.masthead .meta {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    color: var(--muted);
    font-size: 14px;
    font-weight: 800;
    letter-spacing: 0.18em;
}

.masthead h1 {
    margin: 24px 0 0;
    max-width: 900px;
    font-size: clamp(40px, 8vw, 88px);
    line-height: 0.95;
    font-weight: 900;
    letter-spacing: 0;
}

.masthead .subtitle {
    margin: 16px 0 0;
    font-size: 18px;
    font-weight: 800;
    color: var(--body);
}

/* ---- 栏目条 ---- */
.kicker {
    margin: 28px 0 20px;
    border-top: 2px solid var(--ink);
    border-bottom: 2px solid var(--ink);
    padding: 8px 0;
    color: var(--muted);
    text-align: center;
    font-size: 14px;
    font-weight: 800;
    letter-spacing: 0.26em;
}

/* ---- 区块分割线 ---- */
.section-rule { border-bottom: 4px solid var(--ink); padding: 26px 0; }
.part { border-bottom: 4px solid var(--ink); padding-bottom: 24px; }
.part-number { color: var(--accent); font-size: 14px; font-weight: 900; letter-spacing: 0.24em; }

/* ---- 社论摘句 ---- */
.core-left {
    margin-top: 22px;
    border-left: 8px solid var(--ink);
    padding-left: 16px;
    font-size: 22px;
    line-height: 1.55;
    font-weight: 900;
}
.core-invert {
    margin-top: 22px;
    border: 2px solid var(--ink);
    background: var(--ink);
    color: var(--paper);
    padding: 16px;
    font-size: 22px;
    line-height: 1.55;
    font-weight: 900;
}

/* ---- 按钮(padding/font-size 可在使用处覆盖)---- */
.btn {
    border: 2px solid var(--ink);
    background: var(--paper);
    color: var(--ink);
    text-decoration: none;
    padding: 9px 18px;
    font-size: 15px;
    font-weight: 900;
    font-family: inherit;
    letter-spacing: 0.06em;
    white-space: nowrap;
    cursor: pointer;
}
.btn:hover { background: var(--ink); color: var(--paper); }
.btn-solid { background: var(--ink); color: var(--paper); }
.btn-solid:hover { background: var(--paper); color: var(--ink); }
.btn.disabled {
    background: var(--paper);
    color: var(--muted);
    border-color: var(--muted);
    cursor: not-allowed;
}

/* ---- 徽标 ---- */
.badge {
    display: inline-block;
    border: 2px solid var(--ink);
    background: var(--ink);
    color: var(--paper);
    padding: 2px 10px;
    font-size: 13px;
    font-weight: 800;
    letter-spacing: 0.06em;
    margin-left: 8px;
}

/* ---- 信息框(不是现代卡片:方角、无阴影)---- */
.card { border: 2px solid var(--ink); padding: 16px 18px; }

/* ---- 空状态 ---- */
.no-data {
    border: 2px solid var(--ink);
    text-align: center;
    padding: 40px 20px;
    font-size: 18px;
    font-weight: 800;
}
.no-data .hint { font-size: 15px; font-weight: 800; color: var(--muted); margin-top: 10px; }

/* ============================================================
   组件 1:复制提示 toast(配合 newspaper.js)
   ============================================================ */
.toast {
    position: fixed;
    left: 50%;
    bottom: 32px;
    transform: translateX(-50%);
    border: 2px solid var(--ink);
    background: var(--ink);
    color: var(--paper);
    padding: 12px 24px;
    font-size: 15px;
    font-weight: 900;
    letter-spacing: 0.1em;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s ease;
    z-index: 1000;
}
.toast.show { opacity: 1; }

/* ============================================================
   组件 2:自定义下拉(配合 newspaper.js)
   原生 <select class="np-select"> 作为数据源与无 JS 兜底;
   JS 启用后隐藏它、生成 .custom-select 列表。
   需在 <head> 尽早执行 document.documentElement.className += ' js'。
   ============================================================ */
.js select.np-select { display: none; }

.custom-select { position: relative; width: 100%; }

.custom-select .cs-trigger {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    width: 100%;
    padding: 10px 12px;
    border: 2px solid var(--ink);
    border-radius: 0;
    background: var(--paper);
    color: var(--ink);
    font-family: inherit;
    font-size: 16px;
    font-weight: 800;
    text-align: left;
    cursor: pointer;
}
.custom-select.open .cs-trigger { border-color: var(--accent); }

.custom-select .cs-arrow {
    flex: none;
    width: 0;
    height: 0;
    border-left: 6px solid transparent;
    border-right: 6px solid transparent;
    border-top: 7px solid var(--ink);
}
.custom-select.open .cs-arrow { transform: rotate(180deg); }

.custom-select .cs-menu {
    position: absolute;
    left: 0;
    right: 0;
    top: calc(100% - 2px);
    z-index: 30;
    margin: 0;
    padding: 0;
    list-style: none;
    border: 2px solid var(--ink);
    background: var(--paper);
    max-height: 280px;
    overflow-y: auto;
    display: none;
    scrollbar-width: thin;
    scrollbar-color: var(--ink) var(--paper);
}
.custom-select.open .cs-menu { display: block; }

.custom-select .cs-menu::-webkit-scrollbar { width: 12px; }
.custom-select .cs-menu::-webkit-scrollbar-track { background: var(--paper); border-left: 2px solid var(--ink); }
.custom-select .cs-menu::-webkit-scrollbar-thumb { background: var(--ink); border: 2px solid var(--paper); }
.custom-select .cs-menu::-webkit-scrollbar-thumb:hover { background: var(--accent); }

.custom-select .cs-option {
    padding: 10px 12px;
    font-size: 15px;
    font-weight: 800;
    color: var(--body);
    cursor: pointer;
    border-bottom: 1px solid rgba(21, 21, 21, 0.15);
}
.custom-select .cs-option:last-child { border-bottom: none; }
.custom-select .cs-option.selected { color: var(--accent); }
.custom-select .cs-option:hover,
.custom-select .cs-option.active { background: var(--ink); color: var(--paper); }

/* ============================================================
   组件 3:加载遮罩(配合 newspaper.js)
   把内容放进 .np-loading-region(相对定位容器),
   内置一个 .loading-overlay,切换/提交时加 .show。
   ============================================================ */
.np-loading-region { position: relative; min-height: 160px; }

.loading-overlay {
    position: absolute;
    inset: 0;
    background: rgba(243, 239, 228, 0.88);
    display: none;
    align-items: center;
    justify-content: center;
    z-index: 10;
}
.loading-overlay.show { display: flex; }

.loading-box { border: 2px solid var(--ink); background: var(--paper); padding: 24px 32px; text-align: center; }
.loading-box .label { font-size: 18px; font-weight: 900; letter-spacing: 0.2em; color: var(--ink); margin-bottom: 14px; }
.loading-bar { width: 220px; height: 10px; border: 2px solid var(--ink); overflow: hidden; }
.loading-bar::before {
    content: "";
    display: block;
    width: 40%;
    height: 100%;
    background: var(--ink);
    animation: loading-slide 1s linear infinite;
}
@keyframes loading-slide {
    0% { transform: translateX(-110%); }
    100% { transform: translateX(275%); }
}

/* ---- 响应式:860px 断点,多列变单列、保留粗线 ---- */
@media (max-width: 860px) {
    .page { width: calc(100% - 28px); }
}
newspaper.js(三个组件交互逻辑)
/* ============================================================
   报纸版交互组件 (newspaper-style)
   全部基于 class/属性钩子,自初始化,无硬编码 id。
   放在 </body> 前: <script src=".../newspaper.js"></script>
   (另需在 <head> 尽早放:
      <script>document.documentElement.className += ' js';</script>
    以避免原生下拉闪现。本文件也会兜底补上 .js 类。)

   钩子约定:
   - 自定义下拉:    <select class="np-select"> … </select>
   - 改值即提交:    把 select 放进 <form data-np-autosubmit> (可选)
   - 加载遮罩:      表单提交时,自动给页面里的 .loading-overlay 加 .show
   - 复制链接:      <button class="np-copy" data-link="/foo">复制链接</button>
   - 提示 toast:    页面有 <div class="toast"></div> 则复用,否则自动创建
   ============================================================ */
(function () {
    document.documentElement.className += ' js';

    /* ---- toast ---- */
    var toastEl = document.querySelector('.toast');
    var toastTimer = null;
    function ensureToast() {
        if (!toastEl) {
            toastEl = document.createElement('div');
            toastEl.className = 'toast';
            document.body.appendChild(toastEl);
        }
        return toastEl;
    }
    function showToast(msg) {
        var t = ensureToast();
        t.textContent = msg;
        t.classList.add('show');
        if (toastTimer) clearTimeout(toastTimer);
        toastTimer = setTimeout(function () { t.classList.remove('show'); }, 1600);
    }

    /* ---- 加载遮罩 ---- */
    function showLoading() {
        document.querySelectorAll('.loading-overlay').forEach(function (o) { o.classList.add('show'); });
    }
    function hideLoading() {
        document.querySelectorAll('.loading-overlay').forEach(function (o) { o.classList.remove('show'); });
    }
    // 后退/bfcache 恢复时清除残留遮罩
    window.addEventListener('pageshow', hideLoading);
    // 任意表单提交(含原生按钮)都显示遮罩
    document.querySelectorAll('form').forEach(function (f) {
        f.addEventListener('submit', showLoading);
    });

    /* ---- 复制链接 ---- */
    function fallbackCopy(text) {
        var ta = document.createElement('textarea');
        ta.value = text;
        ta.style.position = 'fixed';
        ta.style.opacity = '0';
        document.body.appendChild(ta);
        ta.select();
        try { document.execCommand('copy'); showToast('链接已复制'); }
        catch (e) { showToast('复制失败,请手动复制'); }
        document.body.removeChild(ta);
    }
    document.querySelectorAll('.np-copy').forEach(function (btn) {
        btn.addEventListener('click', function () {
            var link = btn.getAttribute('data-link') || '';
            var full = /^https?:/i.test(link) ? link : (window.location.origin + link);
            if (navigator.clipboard && navigator.clipboard.writeText) {
                navigator.clipboard.writeText(full).then(
                    function () { showToast('链接已复制'); },
                    function () { fallbackCopy(full); }
                );
            } else {
                fallbackCopy(full);
            }
        });
    });

    /* ---- 自定义下拉 ---- */
    function closeAll(except) {
        document.querySelectorAll('.custom-select.open').forEach(function (w) {
            if (w !== except && w._close) w._close();
        });
    }

    function buildSelect(sel) {
        var autosubmit = !!sel.closest('form[data-np-autosubmit]');

        var wrap = document.createElement('div');
        wrap.className = 'custom-select';

        var trigger = document.createElement('button');
        trigger.type = 'button';
        trigger.className = 'cs-trigger';
        trigger.setAttribute('aria-haspopup', 'listbox');
        trigger.setAttribute('aria-expanded', 'false');

        var label = document.createElement('span');
        label.className = 'cs-label';
        var arrow = document.createElement('span');
        arrow.className = 'cs-arrow';
        trigger.appendChild(label);
        trigger.appendChild(arrow);

        var menu = document.createElement('ul');
        menu.className = 'cs-menu';
        menu.setAttribute('role', 'listbox');

        var activeIdx = -1;
        function setActive(i) {
            var items = menu.children;
            for (var k = 0; k < items.length; k++) items[k].classList.toggle('active', k === i);
            activeIdx = i;
            if (i >= 0 && items[i]) items[i].scrollIntoView({ block: 'nearest' });
        }
        function syncLabel() { label.textContent = sel.options[sel.selectedIndex].textContent.trim(); }
        function choose(i) {
            sel.selectedIndex = i;
            syncLabel();
            close();
            sel.dispatchEvent(new Event('change', { bubbles: true }));
        }
        function open() {
            closeAll(wrap);
            wrap.classList.add('open');
            trigger.setAttribute('aria-expanded', 'true');
            setActive(sel.selectedIndex);
        }
        function close() {
            wrap.classList.remove('open');
            trigger.setAttribute('aria-expanded', 'false');
            setActive(-1);
        }
        wrap._close = close;

        Array.prototype.forEach.call(sel.options, function (opt, i) {
            var li = document.createElement('li');
            li.className = 'cs-option' + (opt.selected ? ' selected' : '');
            li.setAttribute('role', 'option');
            li.textContent = opt.textContent.trim();
            li.addEventListener('click', function (e) { e.stopPropagation(); choose(i); });
            menu.appendChild(li);
        });
        syncLabel();

        trigger.addEventListener('click', function (e) {
            e.stopPropagation();
            if (wrap.classList.contains('open')) close(); else open();
        });
        trigger.addEventListener('keydown', function (e) {
            var n = menu.children.length;
            if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
                e.preventDefault();
                if (!wrap.classList.contains('open')) { open(); return; }
                var ni = activeIdx + (e.key === 'ArrowDown' ? 1 : -1);
                if (ni < 0) ni = 0;
                if (ni >= n) ni = n - 1;
                setActive(ni);
            } else if (e.key === 'Enter' || e.key === ' ') {
                if (wrap.classList.contains('open')) {
                    e.preventDefault();
                    if (activeIdx >= 0) choose(activeIdx);
                }
            } else if (e.key === 'Escape') {
                close();
            }
        });

        // 改值即提交(可选):先显示遮罩再提交
        if (autosubmit) {
            sel.addEventListener('change', function () {
                showLoading();
                sel.form.submit();
            });
        }

        wrap.appendChild(trigger);
        wrap.appendChild(menu);
        sel.parentNode.appendChild(wrap);
    }

    document.querySelectorAll('select.np-select').forEach(buildSelect);
    document.addEventListener('click', function () { closeAll(null); });
})();
favicon.svg(模板图标)
<!-- 模板 favicon:极简圆框 + 两字母大字 + 蓝图辅助网格。
     换成团队标识时,改 <text> 里的字母即可(如 BI / NS / XX)。 -->
<svg id="bi-svg-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="100%" height="100%" style="background:#FFFFFF;">
<!-- 极简画布背景 -->
<rect width="512" height="512" fill="#FFFFFF"/>
<!-- 基础圆框容器 -->
<circle cx="256" cy="256" r="232" fill="none" stroke="#000000" stroke-width="8"/>
<!-- 绝对中心对齐核心大字 -->
<text x="254" y="257" text-anchor="middle" dominant-baseline="central" style="font-family: 'Inter', sans-serif; font-weight: 900; letter-spacing: -15px;" font-size="330" fill="#000000">BI</text>
<!-- 辅助设计线 -->
<g opacity="0.2">
<pattern id="icon-grid" width="32" height="32" patternUnits="userSpaceOnUse">
<path d="M 32 0 L 0 0 0 32" fill="none" stroke="#000000" stroke-width="0.5"/>
</pattern>
<rect width="512" height="512" fill="url(#icon-grid)"/>
<line x1="256" y1="0" x2="256" y2="512" stroke="#FF0000" stroke-width="1" stroke-dasharray="2 2"/>
<line x1="0" y1="256" x2="512" y2="256" stroke="#FF0000" stroke-width="1" stroke-dasharray="2 2"/>
<circle cx="256" cy="256" r="236" fill="none" stroke="#000000" stroke-width="0.5" stroke-dasharray="4 4"/>
<circle cx="256" cy="256" r="128" fill="none" stroke="#000000" stroke-width="0.5" stroke-dasharray="4 4"/>
</g>
</svg>
newspaper-demo.html(上面「效果演示」的 demo 源码,自包含、未压缩)
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>报纸版设计系统 demo:文章索引</title>
<!--
  newspaper-demo.html —— newspaper-style 设计系统的交互全家福。
  复现的不变量(对应 newspaper-style.md 的组件文档):
  - 类目下拉是「改值即整页提交」:np-select + data-np-autosubmit,筛选状态进 URL(?cat=xxx)
  - 复制链接按钮复制的是「当前筛选后」的 URL,而不是固定地址
  - 无匹配类目时显示 .no-data 空状态
  样式/交互逻辑均复用同级 newspaper.css / newspaper.js(同一份源文件,不重复定义)。
-->
<script>document.documentElement.className += ' js';</script>
<link rel="stylesheet" href="newspaper.css">
</head>
<body>
<div class="page">

  <header class="masthead">
    <div class="meta"><span>版式实验室 · DEMO</span><span>NO. 07</span></div>
    <h1>报纸版设计系统</h1>
    <p class="subtitle">暖米纸面 · 粗黑分割线 · 强对比排版 —— 交互组件全家福</p>
  </header>

  <div class="kicker">目录 · 共六篇 · 演示用虚构内容</div>

  <div class="section-rule">
    <form method="get" data-np-autosubmit style="display:flex; gap:14px; align-items:flex-end; flex-wrap:wrap;">
      <div style="min-width:220px; flex:1;">
        <div style="font-size:13px; font-weight:800; color:var(--muted); letter-spacing:0.12em; margin-bottom:6px;">按类目筛选</div>
        <select name="cat" class="np-select">
          <option value="">全部类目</option>
          <option value="shizheng">时政</option>
          <option value="caijing">财经</option>
          <option value="wenhua">文化</option>
        </select>
        <noscript><button type="submit" class="btn" style="margin-top:8px;">应用</button></noscript>
      </div>
      <button type="button" class="btn np-copy" data-link="">复制本页链接</button>
    </form>
  </div>

  <div class="np-loading-region">
    <div class="loading-overlay">
      <div class="loading-box"><div class="label">正在加载…</div><div class="loading-bar"></div></div>
    </div>

    <div id="list" style="display:grid; gap:14px; margin-top:22px;">
      <article class="card" data-cat="shizheng">
        <div style="display:flex; align-items:baseline; justify-content:space-between; gap:12px;">
          <h3 style="margin:0; font-size:19px;">总编按:版式即态度<span class="badge">本期头条</span></h3>
          <span style="font-size:13px; color:var(--muted); white-space:nowrap;">时政</span>
        </div>
        <p style="margin:10px 0 0; color:var(--body); line-height:1.6;">粗线分割、无阴影无圆角,靠排版本身建立层级——这是一种克制,也是一种立场。</p>
      </article>

      <article class="card" data-cat="caijing">
        <div style="display:flex; align-items:baseline; justify-content:space-between; gap:12px;">
          <h3 style="margin:0; font-size:19px;">数字纸媒的印刷术乡愁</h3>
          <span style="font-size:13px; color:var(--muted); white-space:nowrap;">财经</span>
        </div>
        <p style="margin:10px 0 0; color:var(--body); line-height:1.6;">屏幕想模仿纸张的重量感,最后学会的却是纸张的规矩:留白、对齐、粗细分明的边界。</p>
      </article>

      <article class="card" data-cat="wenhua">
        <div style="display:flex; align-items:baseline; justify-content:space-between; gap:12px;">
          <h3 style="margin:0; font-size:19px;">宋体重生记:从铅字到网页</h3>
          <span style="font-size:13px; color:var(--muted); white-space:nowrap;">文化</span>
        </div>
        <p style="margin:10px 0 0; color:var(--body); line-height:1.6;">字重与字号的对比,替代了插画和渐变,成为页面上唯一的视觉重音。</p>
      </article>

      <article class="card" data-cat="shizheng">
        <div style="display:flex; align-items:baseline; justify-content:space-between; gap:12px;">
          <h3 style="margin:0; font-size:19px;">会议室里的排版政治<span class="badge">已归档</span></h3>
          <span style="font-size:13px; color:var(--muted); white-space:nowrap;">时政</span>
        </div>
        <p style="margin:10px 0 0; color:var(--body); line-height:1.6;">谁的标题占据头版、谁的引言被摘句放大,从来不只是设计问题。</p>
      </article>

      <article class="card" data-cat="caijing">
        <div style="display:flex; align-items:baseline; justify-content:space-between; gap:12px;">
          <h3 style="margin:0; font-size:19px;">广告位与叙事权的百年拉锯</h3>
          <span style="font-size:13px; color:var(--muted); white-space:nowrap;">财经</span>
        </div>
        <p style="margin:10px 0 0; color:var(--body); line-height:1.6;">版面越干净,越衬得出「这一段是重点」——强对比本身就是一种编辑判断。</p>
      </article>

      <article class="card" data-cat="wenhua">
        <div style="display:flex; align-items:baseline; justify-content:space-between; gap:12px;">
          <h3 style="margin:0; font-size:19px;">留白是最贵的版面</h3>
          <span style="font-size:13px; color:var(--muted); white-space:nowrap;">文化</span>
        </div>
        <p style="margin:10px 0 0; color:var(--body); line-height:1.6;">不装饰,恰恰是最费判断力的装饰选择。</p>
      </article>
    </div>

    <div id="empty" class="no-data" style="display:none; margin-top:22px;">
      本类目暂无文章
      <div class="hint">换个类目试试,或选「全部类目」</div>
    </div>
  </div>

  <div class="core-left">“靠排版建立层级,不用装饰图片、渐变、阴影、圆角。”</div>
  <div class="core-invert">下拉筛选、复制链接、加载遮罩——这三个组件的源码都在 newspaper.js 里,全部基于 class 钩子,无硬编码 id。</div>

  <div class="section-rule" style="display:flex; gap:12px; flex-wrap:wrap; align-items:center;">
    <button class="btn">默认按钮</button>
    <button class="btn btn-solid">实心按钮</button>
    <button class="btn disabled" disabled>禁用按钮</button>
    <span class="badge">徽标示例</span>
  </div>

  <div class="toast"></div>
</div>

<script>
(function () {
  var params = new URLSearchParams(location.search);
  var cat = params.get('cat') || '';
  var select = document.querySelector('select[name="cat"]');
  if (select) select.value = cat;

  var cards = document.querySelectorAll('#list [data-cat]');
  var visible = 0;
  cards.forEach(function (card) {
    var match = !cat || card.getAttribute('data-cat') === cat;
    card.style.display = match ? '' : 'none';
    if (match) visible++;
  });
  document.getElementById('empty').style.display = visible ? 'none' : '';

  var copyBtn = document.querySelector('.np-copy');
  if (copyBtn) copyBtn.setAttribute('data-link', location.pathname + location.search);
})();
</script>
<script src="newspaper.js"></script>
</body>
</html>