diff --git a/AGENTS.md b/AGENTS.md index 80e84ee..ce51e38 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,6 +62,8 @@ - 列表接口必须分页;导出必须遵守同一范围授权,不得绕过筛选或泄露跨组织数据。 - 前端提示不代替后端校验。对异常状态应提供可理解的错误码和可恢复指引。 - 公共看板的数据最小化:只返回活动公开策略允许的数据,并明确标注数据更新时间与统计口径。 +- 公开 CMS 当前固定八类内容:选举公告、规则与政策、候选人公示、选民指南、结果公告、常见问题、资料下载、历史归档。变更内容类型时,必须同步更新 CMS 定义、`PublicContentSections`、首页展示和主菜单入口,不能只新增后台类型或只展示其中三类。 +- 公开端采用中性政务信息风格(白底、政务蓝、冷灰、清晰分隔),以任务和已发布信息为中心;不加载外部字体或追踪脚本,不用装饰性配色掩盖信息层级。八个公开栏目必须有直接可访问的菜单入口,且不得显示未发布内容、未确认计票数据或个人投票信息。 ## 测试与验证 diff --git a/TODO.md b/TODO.md index 8ac2b9a..515f7bc 100644 --- a/TODO.md +++ b/TODO.md @@ -12,7 +12,7 @@ - 身份认证:OpenIddict 或 Keycloak,采用 OpenID Connect / OAuth 2.0 - 实时与任务:SignalR、Hangfire、Redis - 文件:MinIO 或 S3 兼容对象存储 -- 管理端:Vue 3 + Element Plus;公开站点:Nuxt 3(优先) +- 管理端:Vue 3 + Element Plus;公开站点:Nuxt 3(优先;当前基础实现为 Razor Pages 服务端渲染) - 编辑与可视化:CKEditor 5 或 TinyMCE、ECharts、ClosedXML - 可观测性:Serilog + Seq 或 OpenSearch @@ -73,6 +73,8 @@ - [ ] SignalR 推送公开看板数据,断线后使用受控轮询兜底。 - [ ] 公开看板显示活动状态、参与人数、参与率、数据更新时间和公开规则说明。 - [ ] 候选人实时票数、排名和图表只在活动明确开启后公开;不得公开未确认线下结果。 +- [x] CMS 公开信息基础:建立八类固定内容定义(选举公告、规则与政策、候选人公示、选民指南、结果公告、常见问题、资料下载、历史归档)及只读公开内容契约。 +- [x] 公开信息前端:提供八类内容的首页最新发布区、直接栏目菜单入口、目录/详情页、发布内容空状态与服务不可用提示;采用响应式中性政务信息风格。 - [ ] CMS:栏目、文章、公告、政策、操作指南、候选人公示、结果公告、媒体库与附件。 - [ ] CMS 支持草稿、审核、发布、下线、定时发布、版本历史、回滚、分类、标签、SEO。 - [ ] 建立 CMS 内容与选举活动、公开看板、结果公告之间的关联。 diff --git a/scripts/verify-pages.ps1 b/scripts/verify-pages.ps1 index 91f9076..63a8255 100644 --- a/scripts/verify-pages.ps1 +++ b/scripts/verify-pages.ps1 @@ -38,7 +38,7 @@ try { $notice = Invoke-WebRequest -Uri 'http://127.0.0.1:5055/content/notice' -UseBasicParsing -TimeoutSec 5 $interactive = Invoke-WebRequest -Uri 'http://127.0.0.1:5055/ballot-preview' -UseBasicParsing -TimeoutSec 5 - if ($homeResponse.Content -notmatch '公开内容以服务端渲染快速交付') { throw 'Home page content check failed.' } + if ($homeResponse.Content -notmatch '每一项正式信息') { throw 'Home page content check failed.' } if ($notice.Content -notmatch '公开公告模板') { throw 'Notice page content check failed.' } if ($interactive.Content -notmatch '投票交互预览') { throw 'Interactive component content check failed.' } diff --git a/src/ElectionSystem.Api/Modules/Cms/PublicContentSections.cs b/src/ElectionSystem.Api/Modules/Cms/PublicContentSections.cs new file mode 100644 index 0000000..6add4b9 --- /dev/null +++ b/src/ElectionSystem.Api/Modules/Cms/PublicContentSections.cs @@ -0,0 +1,21 @@ +namespace ElectionSystem.Api.Modules.Cms; + +public sealed record PublicContentSection(string ContentType, string Title, string Description); + +public static class PublicContentSections +{ + public static IReadOnlyList All { get; } = + [ + new("ElectionNotice", "选举公告", "事项、安排与正式通知"), + new("PolicyDocument", "规则与政策", "适用规则与制度文件"), + new("CandidateDisclosure", "候选人公示", "候选人公开信息"), + new("VoterGuide", "选民指南", "参与流程与操作说明"), + new("ResultAnnouncement", "结果公告", "已发布的正式结果"), + new("PublicFaq", "常见问题", "公开查阅与参与答疑"), + new("DownloadAsset", "资料下载", "公开资料与表单文件"), + new("ArchiveRecord", "历史归档", "过往活动公开记录") + ]; + + public static bool TryGet(string contentType, out PublicContentSection? section) => + (section = All.FirstOrDefault(item => string.Equals(item.ContentType, contentType, StringComparison.Ordinal))) is not null; +} diff --git a/src/ElectionSystem.Api/Pages/Content/Detail.cshtml b/src/ElectionSystem.Api/Pages/Content/Detail.cshtml index fb42bf9..149f802 100644 --- a/src/ElectionSystem.Api/Pages/Content/Detail.cshtml +++ b/src/ElectionSystem.Api/Pages/Content/Detail.cshtml @@ -12,23 +12,24 @@ - -
+ +
@if (!Model.CmsAvailable) {

信息公开服务暂不可用,请稍后刷新页面。

} else if (Model.Item is { } item) { -

@item.ContentType

+ ← 返回信息公开 +

正式公开内容

@item.Title

@if (item.PublishedUtc is { } publishedUtc) { -

发布时间:

+

发布时间

}
@Html.Raw(item.Html)
}
-
所有时间和结果以活动正式公告为准。
+ diff --git a/src/ElectionSystem.Api/Pages/Content/Index.cshtml b/src/ElectionSystem.Api/Pages/Content/Index.cshtml index 7921751..dc70f02 100644 --- a/src/ElectionSystem.Api/Pages/Content/Index.cshtml +++ b/src/ElectionSystem.Api/Pages/Content/Index.cshtml @@ -12,10 +12,11 @@ - -
-

PUBLIC INFORMATION

+ +
+

信息公开目录

@Model.Heading

+

以下内容均为已审核发布的信息。请以页面所示发布日期及正式公告为准。

@if (!Model.CmsAvailable) {

信息公开服务暂不可用,请稍后刷新页面。

@@ -25,6 +26,6 @@ }
-
所有时间和结果以活动正式公告为准。
+ diff --git a/src/ElectionSystem.Api/Pages/Content/Index.cshtml.cs b/src/ElectionSystem.Api/Pages/Content/Index.cshtml.cs index 36cf380..c0021bb 100644 --- a/src/ElectionSystem.Api/Pages/Content/Index.cshtml.cs +++ b/src/ElectionSystem.Api/Pages/Content/Index.cshtml.cs @@ -6,31 +6,19 @@ namespace ElectionSystem.Api.Pages.Content; public sealed class IndexModel(PublicContentClient publicContentClient) : PageModel { - private static readonly IReadOnlyDictionary Sections = new Dictionary(StringComparer.Ordinal) - { - ["ElectionNotice"] = "选举公告", - ["PolicyDocument"] = "规则与政策", - ["CandidateDisclosure"] = "候选人公示", - ["VoterGuide"] = "选民指南", - ["ResultAnnouncement"] = "结果公告", - ["PublicFaq"] = "常见问题", - ["DownloadAsset"] = "资料下载", - ["ArchiveRecord"] = "历史归档" - }; - public IReadOnlyList Items { get; private set; } = []; public bool CmsAvailable { get; private set; } public string Heading { get; private set; } = "信息公开"; public async Task OnGetAsync(string? contentType, CancellationToken cancellationToken) { - string? sectionName = null; - if (contentType is not null && !Sections.TryGetValue(contentType, out sectionName)) + PublicContentSection? section = null; + if (contentType is not null && !PublicContentSections.TryGet(contentType, out section)) { return NotFound(); } - Heading = sectionName ?? Heading; + Heading = section?.Title ?? Heading; var result = await publicContentClient.ListAsync(contentType, 20, cancellationToken); CmsAvailable = result.IsAvailable; Items = result.Value ?? []; diff --git a/src/ElectionSystem.Api/Pages/Content/Notice.cshtml b/src/ElectionSystem.Api/Pages/Content/Notice.cshtml index f1f6489..790da0b 100644 --- a/src/ElectionSystem.Api/Pages/Content/Notice.cshtml +++ b/src/ElectionSystem.Api/Pages/Content/Notice.cshtml @@ -12,10 +12,11 @@ - -
-

NOTICE

+ +
+

信息公开目录

公开公告

+

仅列出已审核发布的选举公告。相关日期、资格与程序以公告正文为准。

@if (!Model.CmsAvailable) {

公告服务暂不可用,请稍后刷新页面。

@@ -25,6 +26,6 @@ }
-
所有时间和结果以活动正式公告为准。
+ diff --git a/src/ElectionSystem.Api/Pages/Content/_PublicContentList.cshtml b/src/ElectionSystem.Api/Pages/Content/_PublicContentList.cshtml index 1d766bd..94bf5e4 100644 --- a/src/ElectionSystem.Api/Pages/Content/_PublicContentList.cshtml +++ b/src/ElectionSystem.Api/Pages/Content/_PublicContentList.cshtml @@ -11,7 +11,7 @@ else @foreach (var item in Model) {
  • - @item.Title + @item.Title @if (item.PublishedUtc is { } publishedUtc) { diff --git a/src/ElectionSystem.Api/Pages/Index.cshtml b/src/ElectionSystem.Api/Pages/Index.cshtml index 3fdc36a..6c3e05e 100644 --- a/src/ElectionSystem.Api/Pages/Index.cshtml +++ b/src/ElectionSystem.Api/Pages/Index.cshtml @@ -12,37 +12,44 @@ - -
    -

    PUBLIC INFORMATION

    -

    公开内容以服务端渲染快速交付

    -

    公告、候选人公示和正式结果由 CMS 审核发布后,通过 Razor Pages 快速呈现;具体投票和管理交互将按需嵌入 Blazor,并始终由服务端校验资格与规则。

    - 查看信息公开 -
    + +
    +
    +
    +

    公开信息服务

    +

    每一项正式信息,
    都有清晰的公开路径。

    +

    在这里查阅已审核发布的选举公告、候选人公示与结果公告。公开内容由发布流程保障,投票资格与选票始终独立保护。

    + 浏览全部公开信息 +
    +
    +
    +

    正式信息发布流程

    +
      +
    1. 01选举公告说明事项与安排
    2. +
    3. 02候选人公示在公示期内查阅
    4. +
    5. 03结果公告以正式发布为准
    6. +
    +
    @if (!Model.CmsAvailable) { - + } else { -
    -
    -

    NOTICE

    最新公告

    全部公告
    - -
    -
    -

    DISCLOSURE

    候选人公示

    全部公示
    - -
    -
    -

    RESULT

    结果公告

    全部结果
    - -
    -
    +
    +

    LATEST RELEASES

    最新公开信息

    仅展示已审核并对外发布的内容。

    +
    + @foreach (var preview in Model.Sections) + { +
    +

    @preview.Section.Description

    @preview.Section.Title

    查看全部
    + +
    + } +
    +
    } -
    所有时间和结果以活动正式公告为准。
    +
    + diff --git a/src/ElectionSystem.Api/Pages/Index.cshtml.cs b/src/ElectionSystem.Api/Pages/Index.cshtml.cs index 2c0e21c..216f434 100644 --- a/src/ElectionSystem.Api/Pages/Index.cshtml.cs +++ b/src/ElectionSystem.Api/Pages/Index.cshtml.cs @@ -5,21 +5,21 @@ namespace ElectionSystem.Api.Pages; public sealed class IndexModel(PublicContentClient publicContentClient) : PageModel { - public IReadOnlyList Notices { get; private set; } = []; - public IReadOnlyList CandidateDisclosures { get; private set; } = []; - public IReadOnlyList ResultAnnouncements { get; private set; } = []; + public IReadOnlyList Sections { get; private set; } = []; public bool CmsAvailable { get; private set; } public async Task OnGetAsync(CancellationToken cancellationToken) { - var results = await Task.WhenAll( - publicContentClient.ListAsync("ElectionNotice", 3, cancellationToken), - publicContentClient.ListAsync("CandidateDisclosure", 3, cancellationToken), - publicContentClient.ListAsync("ResultAnnouncement", 3, cancellationToken)); + var results = await Task.WhenAll(PublicContentSections.All.Select(section => + publicContentClient.ListAsync(section.ContentType, 3, cancellationToken))); CmsAvailable = results.All(result => result.IsAvailable); - Notices = results[0].Value ?? []; - CandidateDisclosures = results[1].Value ?? []; - ResultAnnouncements = results[2].Value ?? []; + Sections = PublicContentSections.All + .Select((section, index) => new PublicContentSectionPreview(section, results[index].Value ?? [])) + .ToArray(); } } + +public sealed record PublicContentSectionPreview( + PublicContentSection Section, + IReadOnlyList Items); diff --git a/src/ElectionSystem.Api/Pages/Shared/_PublicFooter.cshtml b/src/ElectionSystem.Api/Pages/Shared/_PublicFooter.cshtml new file mode 100644 index 0000000..07aaadc --- /dev/null +++ b/src/ElectionSystem.Api/Pages/Shared/_PublicFooter.cshtml @@ -0,0 +1,6 @@ +
    + +
    diff --git a/src/ElectionSystem.Api/Pages/Shared/_PublicHeader.cshtml b/src/ElectionSystem.Api/Pages/Shared/_PublicHeader.cshtml new file mode 100644 index 0000000..ac4a3a7 --- /dev/null +++ b/src/ElectionSystem.Api/Pages/Shared/_PublicHeader.cshtml @@ -0,0 +1,19 @@ + + diff --git a/src/ElectionSystem.Api/wwwroot/css/site.css b/src/ElectionSystem.Api/wwwroot/css/site.css index 48d1cb4..a535cd2 100644 --- a/src/ElectionSystem.Api/wwwroot/css/site.css +++ b/src/ElectionSystem.Api/wwwroot/css/site.css @@ -1,35 +1,63 @@ -:root { color-scheme: light; font-family: "Microsoft YaHei", system-ui, sans-serif; color: #1c2b38; background: #f6f8f7; } +:root { + color-scheme: light; --ink: #182b3d; --ink-soft: #43576a; --paper: #f4f7fa; --surface: #ffffff; --rule: #d8e0e8; --blue: #1769aa; --blue-deep: #153d61; --blue-pale: #eaf2fa; --muted: #617487; + font-family: "Noto Sans SC", "Microsoft YaHei", system-ui, sans-serif; color: var(--ink); background: var(--paper); +} * { box-sizing: border-box; } -body { margin: 0; min-height: 100vh; } -.site-header { display: flex; justify-content: space-between; align-items: center; gap: 1rem; padding: 1rem max(1.5rem, calc((100vw - 72rem) / 2)); background: #123c4a; color: #fff; } -.site-header a { color: inherit; text-decoration: none; } -.site-header nav { display: flex; gap: 1rem; font-size: .95rem; } -.brand { font-weight: 700; letter-spacing: .02em; } -.container { width: min(72rem, calc(100% - 3rem)); margin: 0 auto; padding: 4rem 0; } -.hero { max-width: 56rem; } -.hero h1 { font-size: clamp(2rem, 5vw, 4rem); line-height: 1.15; margin: .2rem 0 1rem; } -.eyebrow { color: #16717e; font-weight: 700; font-size: .78rem; letter-spacing: .12em; } -.button { display: inline-block; padding: .75rem 1rem; color: #fff; background: #16717e; border-radius: .4rem; text-decoration: none; } -.panel { max-width: 36rem; padding: 1.5rem; border: 1px solid #d6dfdc; background: #fff; border-radius: .6rem; } -.option { display: block; padding: .65rem 0; border-bottom: 1px solid #e8eeeb; } -.error { color: #a02d2d; } -.article { max-width: 52rem; font-size: 1.08rem; line-height: 1.7; } -.disclosure-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.25rem; padding-top: 0; } -.disclosure-section { padding: 1.35rem; background: #fff; border-top: .28rem solid #16717e; box-shadow: 0 1px 0 #d6dfdc; } +html { scroll-behavior: smooth; } +body { margin: 0; min-height: 100vh; background: var(--paper); } +a { text-underline-offset: .18em; } +a:focus-visible, button:focus-visible, input:focus-visible { outline: 3px solid #e5bc69; outline-offset: 3px; } +.skip-link { position: fixed; z-index: 20; top: .5rem; left: .75rem; padding: .6rem .9rem; color: var(--ink); background: #fff; transform: translateY(-170%); } +.skip-link:focus { transform: translateY(0); } +.site-header { color: var(--ink); background: #fff; border-bottom: 1px solid var(--rule); } +.site-header__inner, .site-footer__inner { display: flex; width: min(76rem, calc(100% - 3rem)); margin: 0 auto; align-items: center; justify-content: space-between; gap: 1.5rem; } +.site-header__inner { min-height: 4.5rem; } +.brand { display: inline-flex; align-items: center; gap: .7rem; color: inherit; text-decoration: none; white-space: nowrap; } +.brand strong, .brand small { display: block; } +.brand strong { font-size: 1.08rem; letter-spacing: .03em; }.brand small { margin-top: .12rem; color: var(--muted); font-size: .7rem; letter-spacing: .06em; } +.site-nav { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .15rem .95rem; }.site-nav a { padding: .35rem 0; color: #405569; font-size: .84rem; text-decoration: none; }.site-nav a:hover { color: var(--blue); box-shadow: inset 0 -2px 0 var(--blue); } +.container { width: min(76rem, calc(100% - 3rem)); margin: 0 auto; } +.hero-band { border-bottom: 1px solid var(--rule); background: var(--blue-pale); } +.hero { min-height: 22rem; padding: 4.25rem 0; } +.eyebrow { margin: 0 0 .75rem; color: var(--blue); font-size: .72rem; font-weight: 800; letter-spacing: .12em; } +.hero h1, .article h1, .area-intro h2 { font-family: "Noto Serif SC", SimSun, serif; } +.hero h1 { max-width: 48rem; margin: 0; font-size: clamp(2.35rem, 5.4vw, 4.2rem); font-weight: 600; letter-spacing: -.035em; line-height: 1.15; }.hero h1 em { color: var(--blue); font-style: normal; } +.hero__lead { max-width: 40rem; margin: 1.4rem 0 1.8rem; color: var(--ink-soft); font-size: 1.07rem; line-height: 1.8; } +.button { display: inline-flex; gap: .7rem; align-items: center; padding: .82rem 1.05rem; color: #fff; background: var(--blue); border: 1px solid var(--blue); font-weight: 750; text-decoration: none; transition: background .18s ease, transform .18s ease; }.button:hover { background: #0e5792; transform: translateY(-2px); } +.button span { font-size: 1.2rem; line-height: .7; } +.process-band { padding: 2.8rem 0 2.6rem; } +.process-band__title { margin: 0 0 1rem; color: var(--ink-soft); font-size: .78rem; font-weight: 750; letter-spacing: .12em; } +.process-band ol { display: grid; grid-template-columns: repeat(3, 1fr); margin: 0; padding: 0; list-style: none; } +.process-band li { position: relative; display: grid; grid-template-columns: auto 1fr; gap: .2rem .8rem; padding: 1rem 1.4rem; border-left: 1px solid var(--rule); } +.process-band li:first-child { padding-left: 0; border-left: 0; } +.process-band span { grid-row: span 2; color: var(--blue); font-family: ui-monospace, Consolas, monospace; font-size: .78rem; font-weight: 700; } +.process-band strong { font-size: 1rem; } +.process-band small { color: #63726f; font-size: .8rem; } +.disclosure-area { padding: 1rem 0 5rem; } +.area-intro { display: grid; grid-template-columns: 1fr auto; gap: 0 2rem; align-items: end; margin-bottom: 1.35rem; } +.area-intro .eyebrow { grid-column: 1 / -1; } +.area-intro h2 { margin: 0; font-size: clamp(1.65rem, 3vw, 2.25rem); font-weight: 600; } +.area-intro > p:last-child { max-width: 23rem; margin: 0; color: #60706d; font-size: .92rem; line-height: 1.6; } +.disclosure-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; border: 1px solid var(--rule); background: var(--rule); } +.disclosure-section { min-width: 0; padding: 1.55rem; background: var(--surface); border-top: .24rem solid var(--blue); }.disclosure-section:nth-child(4n + 2) { border-color: #4c7898; }.disclosure-section:nth-child(4n + 3) { border-color: #718aa0; }.disclosure-section:nth-child(4n) { border-color: #96aaba; } .section-heading { display: grid; grid-template-columns: 1fr auto; column-gap: 1rem; align-items: baseline; } -.section-heading .eyebrow { grid-column: 1 / -1; margin: 0 0 .35rem; } -.section-heading h2 { margin: 0; font-size: 1.3rem; } -.section-heading a { color: #12606b; font-size: .88rem; } -.content-list { margin: 1rem 0 0; padding: 0; list-style: none; } -.content-list li { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 1rem; padding: .85rem 0; border-bottom: 1px solid #e8eeeb; } -.content-list a { color: #173d47; font-weight: 650; text-decoration: none; } -.content-list a:hover { color: #16717e; text-decoration: underline; } -.content-list time, .published-at { color: #52616b; font-size: .88rem; white-space: nowrap; } -.status-note, .empty-state { margin: 0; padding: 1rem 1.15rem; color: #35545d; background: #eaf3f2; border-left: .25rem solid #16717e; } -.cms-body { margin-top: 2rem; overflow-wrap: anywhere; } -.cms-body img, .cms-body video { max-width: 100%; height: auto; } -.cms-body table { max-width: 100%; border-collapse: collapse; } -.cms-body th, .cms-body td { padding: .5rem; border: 1px solid #d6dfdc; } -.site-footer { padding: 2rem max(1.5rem, calc((100vw - 72rem) / 2)); color: #52616b; font-size: .9rem; } -@media (max-width: 52rem) { .disclosure-grid { grid-template-columns: 1fr; } } -@media (max-width: 40rem) { .site-header { align-items: flex-start; flex-direction: column; } .container { padding: 2.5rem 0; } } +.section-heading .eyebrow { grid-column: 1 / -1; margin-bottom: .4rem; color: #6d7c77; } +.section-heading h2 { margin: 0; font-family: "Noto Serif SC", SimSun, serif; font-size: 1.36rem; font-weight: 600; } +.section-heading a { color: var(--blue); font-size: .84rem; font-weight: 700; } +.article { max-width: 57rem; padding: 4.5rem 0 5.5rem; font-size: 1.05rem; line-height: 1.8; } +.article h1 { max-width: 48rem; margin: 0; color: var(--ink); font-size: clamp(2rem, 4vw, 3.15rem); font-weight: 600; letter-spacing: -.025em; line-height: 1.28; } +.article__intro { max-width: 44rem; margin: 1.1rem 0 2rem; color: #516461; line-height: 1.8; } +.listing-page { min-height: 34rem; }.detail-page { min-height: 38rem; } +.back-link { display: inline-block; margin-bottom: 1.65rem; color: var(--blue); font-size: .9rem; font-weight: 700; }.published-at { display: inline-flex; gap: .65rem; align-items: center; margin: 1.45rem 0 0; padding: .45rem .75rem; color: #526779; background: var(--blue-pale); font-size: .84rem; line-height: 1.2; } +.published-at span { font-weight: 750; } +.content-list { margin: 1.5rem 0 0; padding: 0; list-style: none; border-top: 1px solid var(--rule); } +.content-list li { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 1rem; align-items: baseline; padding: 1rem 0; border-bottom: 1px solid var(--rule); } +.content-list a { display: flex; gap: .7rem; justify-content: space-between; color: var(--ink); font-weight: 700; line-height: 1.55; text-decoration: none; } +.content-list a:hover { color: var(--blue); }.content-list__arrow { flex: 0 0 auto; color: var(--blue); font-size: 1.08rem; transition: transform .18s ease; }.content-list a:hover .content-list__arrow { transform: translate(2px, -2px); } +.content-list time { color: #64716f; font-family: ui-monospace, Consolas, monospace; font-size: .76rem; white-space: nowrap; } +.status-note, .empty-state { display: grid; gap: .25rem; margin: 1.6rem 0 0; padding: 1rem 1.15rem; color: #36526a; background: var(--blue-pale); border-left: .25rem solid var(--blue); font-size: .95rem; }.status-note strong { color: var(--ink); }.cms-body { margin-top: 2.4rem; padding-top: 2rem; border-top: 1px solid var(--rule); overflow-wrap: anywhere; }.cms-body :is(h2, h3, h4) { margin: 2rem 0 .75rem; color: var(--ink); font-family: "Noto Serif SC", SimSun, serif; line-height: 1.4; }.cms-body a { color: var(--blue); font-weight: 650; }.cms-body img, .cms-body video { max-width: 100%; height: auto; }.cms-body table { display: block; max-width: 100%; overflow-x: auto; border-collapse: collapse; }.cms-body th, .cms-body td { padding: .6rem .75rem; border: 1px solid var(--rule); text-align: left; }.cms-body th { background: #eef4f8; } +.site-footer { color: #c4d0da; background: #23313f; }.site-footer__inner { min-height: 5.2rem; color: #c4d0da; font-size: .82rem; }.site-footer__inner span:first-child { color: #fff; letter-spacing: .04em; } +@media (max-width: 65rem) { .disclosure-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +@media (max-width: 52rem) { .site-header__inner { align-items: flex-start; flex-direction: column; justify-content: center; padding: .9rem 0; }.site-nav { justify-content: flex-start; }.hero { min-height: 0; padding: 4.2rem 0 4rem; }.hero::after { right: -16rem; }.process-band ol, .disclosure-grid { grid-template-columns: 1fr; }.process-band li, .process-band li:first-child { padding: .85rem 0; border-top: 1px solid var(--rule); border-left: 0; }.process-band li:first-child { border-top: 0; }.area-intro { grid-template-columns: 1fr; gap: .75rem; }.disclosure-section { padding: 1.3rem; } } +@media (max-width: 40rem) { .site-header__inner, .site-footer__inner, .container { width: min(100% - 2rem, 76rem); }.site-nav { gap: .15rem .8rem; }.site-nav a { font-size: .8rem; }.hero { padding: 3.5rem 0; }.hero h1 { font-size: clamp(2.05rem, 11vw, 3rem); }.article { padding: 3rem 0 4rem; }.content-list li { grid-template-columns: 1fr; gap: .35rem; }.content-list time { order: -1; }.site-footer__inner { align-items: flex-start; flex-direction: column; justify-content: center; gap: .35rem; padding: 1rem 0; } } +@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; } }