summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/controllers/csp_reports_controller.rb4
-rw-r--r--app/models/concerns/file_attachment.rb3
-rw-r--r--app/views/assets/show.html.erb10
-rw-r--r--app/views/layouts/application.html.erb2
-rw-r--r--app/views/layouts/events.html.erb17
-rw-r--r--app/views/nodes/new.html.erb5
-rw-r--r--config/initializers/csp_log.rb4
-rw-r--r--config/routes.rb8
-rw-r--r--doc/DESIGN_HISTORY.md200
-rw-r--r--public/javascripts/admin_interface.js21
-rw-r--r--public/javascripts/admin_search.js6
-rw-r--r--public/stylesheets/ccc.css749
-rw-r--r--public/stylesheets/ccc.css_579
-rw-r--r--public/stylesheets/ccc.css_old257
14 files changed, 670 insertions, 1195 deletions
diff --git a/app/controllers/csp_reports_controller.rb b/app/controllers/csp_reports_controller.rb
index a8f8edb..5a3b55e 100644
--- a/app/controllers/csp_reports_controller.rb
+++ b/app/controllers/csp_reports_controller.rb
@@ -12,9 +12,9 @@ class CspReportsController < ApplicationController
12 if report 12 if report
13 directive = report["effective-directive"] || report["violated-directive"] 13 directive = report["effective-directive"] || report["violated-directive"]
14 at = (URI.parse(report["document-uri"]).path rescue "unparsed") 14 at = (URI.parse(report["document-uri"]).path rescue "unparsed")
15 Rails.logger.warn("CSP violation: #{directive} blocked=#{report['blocked-uri']} at=#{at}") 15 CSP_LOGGER.warn("CSP violation: #{directive} blocked=#{report['blocked-uri']} at=#{at}")
16 else 16 else
17 Rails.logger.warn("CSP violation: unparseable report (#{raw.to_s.bytesize} bytes)") 17 CSP_LOGGER.warn("CSP violation: unparseable report (#{raw.to_s.bytesize} bytes)")
18 end 18 end
19 19
20 head :no_content 20 head :no_content
diff --git a/app/models/concerns/file_attachment.rb b/app/models/concerns/file_attachment.rb
index 613a6f0..3c09456 100644
--- a/app/models/concerns/file_attachment.rb
+++ b/app/models/concerns/file_attachment.rb
@@ -106,7 +106,8 @@ module FileAttachment
106 end 106 end
107 107
108 def sanitize_filename(filename) 108 def sanitize_filename(filename)
109 File.basename(filename).gsub(/[^\w\.\-]/, '_') 109 name = File.basename(filename).unicode_normalize(:nfc)
110 name.gsub(/(?u)[^\w\.\-]/, '_')
110 end 111 end
111 112
112 # Proxy object returned by asset.upload, providing the Paperclip-compatible 113 # Proxy object returned by asset.upload, providing the Paperclip-compatible
diff --git a/app/views/assets/show.html.erb b/app/views/assets/show.html.erb
index 5717dd9..ff00883 100644
--- a/app/views/assets/show.html.erb
+++ b/app/views/assets/show.html.erb
@@ -20,7 +20,15 @@
20 <div class="node_content"><%= image_tag @asset.upload.url(:medium), style: "max-width: 300px; max-height: 300px;" %></div> 20 <div class="node_content"><%= image_tag @asset.upload.url(:medium), style: "max-width: 300px; max-height: 300px;" %></div>
21 21
22 <div class="node_description">Public Path</div> 22 <div class="node_description">Public Path</div>
23 <div class="node_content"><%= @asset.upload.url.sub(/\?\d+$/, "") %></div> 23 <div class="node_content">
24 <% public_path = @asset.upload.url.sub(/\?\d+$/, "") %>
25 <%= link_to public_path, public_path, target: "_blank", rel: "noopener" %>
26 <button type="button" class="action_button copy_button"
27 data-copy-url="<%= request.base_url + public_path %>">
28 <%= icon("copy", library: "tabler", "aria-hidden": true) %>
29 <span class="copy_button_label">Copy URL</span>
30 </button>
31 </div>
24 32
25 <div class="node_description">Content Type</div> 33 <div class="node_description">Content Type</div>
26 <div class="node_content"><%= @asset.upload.content_type %></div> 34 <div class="node_content"><%= @asset.upload.content_type %></div>
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index ca867ab..f981fc1 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -62,12 +62,10 @@
62 <div id="center_column"> 62 <div id="center_column">
63 <%= yield :layout %> 63 <%= yield :layout %>
64 <div id="footer"> 64 <div id="footer">
65 <br />
66 <p style="text-align: center"> 65 <p style="text-align: center">
67 <%= link_to "Impressum", content_path("imprint") %> 66 <%= link_to "Impressum", content_path("imprint") %>
68 <%= link_to "Datenschutz", content_path("datenschutz") %> 67 <%= link_to "Datenschutz", content_path("datenschutz") %>
69 <%= language_selector %> 68 <%= language_selector %>
70 <!-- %= link_to t(:sponsors), content_path("sponsors") % -->
71 </p> 69 </p>
72 </div> 70 </div>
73 </div> 71 </div>
diff --git a/app/views/layouts/events.html.erb b/app/views/layouts/events.html.erb
deleted file mode 100644
index 93a6c0c..0000000
--- a/app/views/layouts/events.html.erb
+++ /dev/null
@@ -1,17 +0,0 @@
1<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3
4<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
5<head>
6 <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
7 <title>Events: <%= controller.action_name %></title>
8 <%= stylesheet_link_tag 'scaffold' %>
9</head>
10<body>
11
12<p style="color: green"><%= flash[:notice] %></p>
13
14<%= yield %>
15
16</body>
17</html>
diff --git a/app/views/nodes/new.html.erb b/app/views/nodes/new.html.erb
index 890d46e..805fbc9 100644
--- a/app/views/nodes/new.html.erb
+++ b/app/views/nodes/new.html.erb
@@ -41,7 +41,10 @@
41 <div class="node_description">Resulting path</div> 41 <div class="node_description">Resulting path</div>
42 <div class="node_content"> 42 <div class="node_content">
43 <span id="resulting_path">—</span> 43 <span id="resulting_path">—</span>
44 <button type="button" id="copy_resulting_path" class="unselected">copy</button> 44 <button type="button" id="copy_resulting_path" class="unselected copy_button" data-copy-target="#resulting_path">
45 <span class="copy_button_label">copy</span>
46 </button>
47
45 <span class="field_hint">This preview updates as you type. The final path can still be changed afterward by editing the title (for the last segment) or moving the node to a different parent (for everything before it).</span> 48 <span class="field_hint">This preview updates as you type. The final path can still be changed afterward by editing the title (for the last segment) or moving the node to a different parent (for everything before it).</span>
46 </div> 49 </div>
47 50
diff --git a/config/initializers/csp_log.rb b/config/initializers/csp_log.rb
new file mode 100644
index 0000000..1dde9c4
--- /dev/null
+++ b/config/initializers/csp_log.rb
@@ -0,0 +1,4 @@
1# CSP violation reports get their own file, independent of config.logger.
2CSP_LOGGER = Rails.env.production? ?
3 ActiveSupport::Logger.new(Rails.root.join("log", "csp_violations.log")) :
4 Rails.logger
diff --git a/config/routes.rb b/config/routes.rb
index 743b46e..6ddf48c 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -109,11 +109,11 @@ Cccms::Application.routes.draw do
109 109
110 resource :session 110 resource :session
111 111
112 get 'rss/updates', :to => 'rss#updates', :as => :rss 112 get 'rss/updates', :to => 'rss#updates', :as => :rss, :defaults => { :format => :xml }
113 get 'rss/updates.:format', :to => 'rss#updates', :as => :rss_feed, 113 get 'rss/updates.:format', :to => 'rss#updates', :as => :rss_feed, :defaults => { :format => :xml },
114 :constraints => { :format => /xml|rdf/ } 114 :constraints => { :format => /xml|rdf/ }
115 get 'rss/tags/:tag/updates', :to => 'rss#tag_updates', :as => :rss_tag 115 get 'rss/tags/:tag/updates', :to => 'rss#tag_updates', :as => :rss_tag, :defaults => { :format => :xml }
116 get 'rss/tags/:tag/updates.:format', :to => 'rss#tag_updates', :as => :rss_tag_feed, 116 get 'rss/tags/:tag/updates.:format', :to => 'rss#tag_updates', :as => :rss_tag_feed, :defaults => { :format => :xml },
117 :constraints => { :format => /xml/ } 117 :constraints => { :format => /xml/ }
118 118
119 match 'galleries/*page_path' => 'content#render_gallery', :via => :get 119 match 'galleries/*page_path' => 'content#render_gallery', :via => :get
diff --git a/doc/DESIGN_HISTORY.md b/doc/DESIGN_HISTORY.md
new file mode 100644
index 0000000..5eb664b
--- /dev/null
+++ b/doc/DESIGN_HISTORY.md
@@ -0,0 +1,200 @@
1## The head / draft / autosave / translation lifecycle
2
3### Era 1 — the original design (2009, `doc/README_FOR_APP`).
4
5Two layers only: `head` (the published, public revision) and `draft`
6(a full copy of head's content, created the moment someone starts
7editing).
8
9No separate lock concept. The draft's *author* was the lock.
10Pessimistic, one editor at a time, enforced by ownership rather than a
11distinct column. An author could withdraw authorship (the draft
12becomes author-less, open for someone else to pick up) or discard the
13draft entirely, reverting to head.
14
15Admins could override any lock or remove any stuck draft on another
16editor's behalf. No autosave.
17
18Globalize wasn't part of the permission model (`Permission` grants
19admin rights to steal a lock a node.
20
21### Why a third layer got added.
22
23Under the original model, the moment someone started typing, a real,
24numbered revision already existed: `acts_as_list` assigns revision
25numbers at creation, scoped to `node_id`.
26
27A speculative edit (open the editor, type a few words, close the tab)
28still counted as a real Draft, and a real future revision if it were
29ever published.
30
31The rebuild introduced `autosave` as a genuinely separate layer: the
32same `Page` model, but created with `node_id: nil` specifically so
33it's excluded from `Node#pages`, the actual revision list.
34
35Typing progress survives a crash or a closed tab, but nothing becomes
36a real revision until the editor deliberately saves, which promotes
37the autosave's full content, wholesale, into the draft layer via
38`clone_attributes_from`.
39
40A canonical six-state reference table (head / draft / autosave present
41or absent, and what each combination permits) lives alongside the
42model code as the authority for any future work on this lifecycle.
43
44### Translations (Globalize), layered on top
45
46The governing principle for admin views from that rebuild:
47
48*presentation locale* (what language the admin chrome renders in) and
49*content-target locale* (which translation a given screen reads or
50writes) are separate concerns that must never share a mechanism.
51
52The route's `:locale` segment governs only the former. A locale is
53either the default one — edited exclusively through the primary node
54editor, pinned via `Globalize.with_locale` regardless of what the
55route happens to say, so a stray `/en/` URL can't silently edit the
56German content (as has been the case before the rewrite in 2026), or
57one of the non-default locales, edited exclusively through a
58Translations sub-resource under a deliberately distinct route
59parameter (`translation_locale`), chosen specifically because it must
60never leak into `default_url_options` the way the chrome locale does.
61Never both paths for the same locale.
62
63For now, admin chrome language stays tied to the route locale for now.
64A per-editor preference column was considered and set aside as
65revisit-later, not built.
66
67Translations carry no independent publish state: a translation goes
68live exactly when the draft containing it is promoted to head, same as
69everything else in that `Page` row. Both a per-translation
70`published_at` and full per-locale draft/head/autosave parity with
71`Node` were considered and rejected as unneeded machinery absent a
72real editorial need for staggered release timing.
73
74## Events and the calendar
75
76### Problem
77
78Calendar entries required an internal Node — every event needed a page
79somewhere in the tree. Editors avoided this: either the updates stream
80absorbed non-news content, or a page got created with no natural place
81in the navigation. The calendar widget went dormant rather than get
82used under those terms.
83
84### Rebuild.
85
86The Event model was rewritten around the RRULE humanizer
87(`app/models/concerns/rrule_humanizer.rb`) and a picker UI bound to it
88by a deliberate scope rule: the picker only generates or reads back
89RRULE shapes the humanizer can also render as prose. A picker that
90could express more than the humanizer can describe would let an editor
91create a schedule that renders as blank text on the public page —
92worse than a raw string the editor had to type by hand.
93`Event#node_id` became optional, so an event can point outward (an
94external URL) instead of requiring a page. This is what made reviving
95the calendar possible at all.
96
97### The "where to put an event" problem
98
99Once events no longer needed a node, turning the calendar on directly
100would have meant every chapter's regular open evening becoming an
101entry — a handful of one-off items buried under dozens of recurring,
102low-signal rows.
103
104### Resolution:
105
106Two widgets, not one:
107
108- `open_erfas_today` took the recurring, date-filtered case — which
109 chapters are open today. Its label went through rejected drafts
110 before landing on phrasing that admits it's a curated sample, not a
111 complete listing.
112- `div#frontpage_calendar` was reserved for the one-off case —
113 conferences, memorial gatherings. Its CSS carries an unconditional
114 `display: none` with no override anywhere in the stylesheet — not a
115 bug, a widget staged for a UI that hasn't been built yet.
116
117## The action log (`NodeAction`)
118
119### Origin
120
121The action log grew directly out of an audit of what timestamps
122actually mean: Two things were missing in the old model: no record of
123who actually clicked Publish (`page.editor` reflects the last content
124save, not necessarily that specific act), and the dashboard's
125recent-changes widget was actively wrong, showing an unrelated
126in-progress draft's byline next to a timestamp that didn't correspond
127to why the node appeared on the list at all.
128
129The log was designed to fill the gap and to answer "what did people
130do," a question the existing timestamp fields were never designed to
131answer.
132
133### Governing principle
134
135A derived log is only as reliable as the discipline that maintains it.
136
137Named explicitly before any code was written, not discovered afterward.
138Three ways it can drift:
139
1401. A future code path can simply forget to write to it — this project
141 had already found that exact shape twice (`wipe_draft!`'s ungated
142 branch; `Page.aggregate` silently ignoring a `conditions=` argument).
1432. If the log write and the actual mutation aren't in the same
144 transaction, they can diverge on a crash. Resolved by writing from
145 *inside* the model methods themselves (`autosave!`, `save_draft!`,
146 `publish_draft!`, `trash!`, `destroy!`) rather than from the
147 controllers that call them — every future caller is covered for
148 free, the same reasoning already proven by the JS-autosave work.
1493. It can never retroactively reconstruct anything from before it
150 existed — addressed deliberately by the backfill (below), not
151 ignored.
152
153### Avoiding noise
154
155A draft save or publish that produces no visible difference shouldn't
156create a visible log entry. Solved by reusing
157`Page#diff_against` / `has_changes_to?` — comparisons that already
158existed and were already tested — rather than inventing new diffing
159logic for the log to own.
160
161### The verb vocabulary
162
163Firstly, the log deliberately excludes locks (transient, no history
164value), autosave and draft-saving itself (exactly the noise the log
165exists to filter out), and tag/asset/event edits made *on a draft*
166(those will later surface as changed-flags at publish time, which
167is when they become real).
168
169Deliberately included, on reflection: reverts and discards (arguably
170more worth tracking than promotions: "who discarded this work, and
171when" is the fact someone would actually go looking for), shared
172preview link generation and revocation (security-relevant, handing
173unpublished content to an outsider), and slug/path changes
174(link-breaking, earns its own record separate from ordinary edits).
175
176### Rollback isn't a separate verb.
177
178`restore_revision!` writes a `publish` entry with a discriminator
179(`via: "revision"` instead of `via: "draft"`) rather than its own
180vocabulary. One verb, one meaning ("a page got promoted to head"),
181distinguished by how it got there.
182
183### Backfill
184
185Historical entries mirror the live vocabulary exactly. Diff content
186(what actually changed) is computed from the real historical revision
187data, since consecutive `Page` rows already existed to compare — only
188the actor and the timestamp are ever inferred, each backfilled entry
189carrying an `inferred_from` field naming the specific heuristic used
190(e.g. "from a page revision," "from `published_at`"). A `null`
191`inferred_from` means the entry was witnessed live, not reconstructed —
192so the log can always answer, for any entry, how much to trust it.
193
194### The full metadata contract per verb
195
196`create`, `publish`, `move`, `trash`, `restore_from_trash`, `destroy`
197— lives as a code comment directly above `record!` in
198`app/models/node.rb`. That's the authoritative, current version; this
199entry explains why it's shaped the way it is, not what it says field
200by field.
diff --git a/public/javascripts/admin_interface.js b/public/javascripts/admin_interface.js
index b7bdc10..e9f96c2 100644
--- a/public/javascripts/admin_interface.js
+++ b/public/javascripts/admin_interface.js
@@ -86,6 +86,27 @@ $(document).ready(function () {
86 if (meta) request.setRequestHeader("X-CSRF-Token", meta.content); 86 if (meta) request.setRequestHeader("X-CSRF-Token", meta.content);
87 }); 87 });
88 88
89 document.addEventListener('click', function (event) {
90 var button = event.target.closest('.copy_button');
91 if (!button) return;
92
93 var text = button.dataset.copyUrl;
94 if (text === undefined && button.dataset.copyTarget) {
95 var target = document.querySelector(button.dataset.copyTarget);
96 text = target ? target.textContent : undefined;
97 }
98
99 if (!text || text === '—' || !navigator.clipboard) return;
100
101 navigator.clipboard.writeText(text).then(function () {
102 var label = button.querySelector('.copy_button_label');
103 if (!label) return;
104 var original = label.textContent;
105 label.textContent = 'Copied!';
106 setTimeout(function () { label.textContent = original; }, 1500);
107 });
108 });
109
89}); 110});
90 111
91 112
diff --git a/public/javascripts/admin_search.js b/public/javascripts/admin_search.js
index b53c931..ad368cb 100644
--- a/public/javascripts/admin_search.js
+++ b/public/javascripts/admin_search.js
@@ -202,12 +202,6 @@ parent_search = {
202 $("#title").bind("input", function() { 202 $("#title").bind("input", function() {
203 parent_search.update_resulting_path(); 203 parent_search.update_resulting_path();
204 }); 204 });
205
206 $("#copy_resulting_path").bind("click", function() {
207 var path = $("#resulting_path").text();
208 if (path === "—" || !navigator.clipboard) return;
209 navigator.clipboard.writeText(path);
210 });
211 }, 205 },
212 206
213 update_resulting_path : function() { 207 update_resulting_path : function() {
diff --git a/public/stylesheets/ccc.css b/public/stylesheets/ccc.css
index cbeea17..a07cc4d 100644
--- a/public/stylesheets/ccc.css
+++ b/public/stylesheets/ccc.css
@@ -1,3 +1,55 @@
1/* ==========================================================================
2 ccc.css — annotated demolition survey (behavior-preserving rewrite)
3 ==========================================================================
4 Sorted into nine concerns; each banner carries an ASSUMES: line naming
5 the tokens and images it depends on. Within any equal-specificity
6 collision set, relative source order is preserved verbatim.
7 ========================================================================== */
8
9/* ==========================================================================
10 1. DESIGN TOKENS
11 ==========================================================================
12 The desktop layout is a fixed lattice drawn around two images:
13 header.png (909px wide, 145px tall) and left_column.png (continues the
14 header artwork down the left menu band via a wrapper-covering ::before).
15 Change these tokens and the two images must be regenerated — that IS
16 the redesign.
17 NOTE: custom properties cannot appear in @media conditions, so the
18 1015/1016 breakpoint below stays a literal pair. Its provenance is
19 unexplained (roughly --page-width plus a margin allowance).
20 ========================================================================== */
21
22:root {
23 --page-width: 909px; /* header.png's drawn width; the wrapper.
24 Decomposes: 185 left band (135 col + 50 pad)
25 + 490 center at left 200 + 210 right band
26 (155 col + 55 pad) at left 690. 690 + 210
27 = 900: 9px absorbed silently at the right
28 edge. */
29 --header-height: 145px; /* header.png's pixel height; search box and
30 light-mode toggle hang from its bottom
31 edge. */
32 --left-col-width: 135px; /* left band = this + --left-col-pad = 185px */
33 --left-col-pad: 50px;
34 --center-left: 200px; /* 15px gutter beyond the 185px left band */
35 --center-width: 490px;
36 --right-left: 690px; /* = --center-left + --center-width */
37 --right-width: 155px; /* right band = this + --right-pad = 210px */
38 --right-pad: 55px;
39 --search-left: 676px; /* provenance unknown; sits 14px left of
40 --right-left */
41 --toggle-left: 816px; /* provenance unknown; 140px right of
42 --search-left */
43 --burger-right: 54px; /* provenance unknown */
44}
45
46/* ==========================================================================
47 2. RESET & DOCUMENT SCAFFOLD
48 ==========================================================================
49 ASSUMES: nothing from the lattice. Element defaults and generic
50 utilities only.
51 ========================================================================== */
52
1html { 53html {
2 height: 100%; 54 height: 100%;
3 line-height: 1.6rem; 55 line-height: 1.6rem;
@@ -17,6 +69,54 @@ body {
17 height: 100%; 69 height: 100%;
18} 70}
19 71
72img {
73 max-width: 100%;
74}
75
76pre {
77 overflow: auto;
78}
79
80li {
81 line-height: 1.5rem;
82 margin-block-start: 1rem;
83 margin-block-end: 1rem;
84}
85
86dt {
87 font-weight: bold;
88 margin-bottom: 1em;
89}
90
91dd {
92 margin-bottom: 1em;
93}
94
95.pagination {
96 margin-bottom: .5rem;
97}
98
99+ /* Single caller: _headline_image.html.erb's image-count caption
100+ ("N Bilder"), shown only when a gallery has more than one image. */
101.right {
102 text-align: right;
103}
104
105.hide-me {
106 display: none;
107}
108
109/* ==========================================================================
110 3. COLOR SCHEME & LIGHT/DARK TOGGLE
111 ==========================================================================
112 ASSUMES: --header-height, --toggle-left (desktop toggle position);
113 header.png and left_column.png (the invert(50%) treatment); the nonced
114 inline restore script in the layout that re-checks #light-mode before
115 first paint (the JS contract: checkbox id "light-mode", state persisted
116 by the script). Symmetric blocks: each scheme inverts when the toggle
117 is checked.
118 ========================================================================== */
119
20@media (prefers-color-scheme: light) { 120@media (prefers-color-scheme: light) {
21 body:has(#light-mode:checked) { 121 body:has(#light-mode:checked) {
22 color-scheme: dark; 122 color-scheme: dark;
@@ -49,114 +149,221 @@ body {
49 } 149 }
50} 150}
51 151
52img { 152input#light-mode[type="checkbox"] {
53 max-width: 100%; 153 display: none;
54} 154}
55 155
56div#header img { 156label[for=light-mode] {
57 border: none; 157 font-size: 25px;
158 user-select: none;
159 cursor: pointer;
160 filter: grayscale(1) contrast(10%);
58} 161}
59 162
60@media(min-width:1016px) { 163/* Light and dark mode button magic */
61 div#wrapper { 164@media(max-width:1015px) {
62 position: relative; 165 div#light-mode-div {
63 width: 909px; 166 position: absolute;
64 margin: 0 auto 0 auto; 167 left: 170px; /* mobile toolbar offset; provenance unknown — anchors to
65 text-align: left; 168 the viewport, not #toolbox (see section 5) */
66 min-height: 100%;
67 }
68 .break-mobile {
69 display: block;
70 } 169 }
71} 170}
72 171
73pre { 172@media(min-width:1016px) {
74 overflow: auto; 173 div#light-mode-div {
75} 174 position: absolute;
76 175 top: var(--header-height); /* bottom edge of header.png */
77.right { 176 left: var(--toggle-left);
78 text-align: right; 177 }
79} 178}
80 179
81/*------------------links-------------------*/ 180/* ==========================================================================
181 4. LINKS & HEADINGS
182 ==========================================================================
183 ASSUMES: nothing from the lattice. Site-wide type and link colors;
184 the orange is the one brand color that survives theming.
185 ========================================================================== */
82 186
83a { 187a {
84 color: #F8921E; 188 color: #F8921E;
85 text-decoration: none; 189 text-decoration: none;
86} 190}
87 191
88a:visited {
89 color: #D1791A;
90 text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
91}
92
93a:hover { 192a:hover {
94 color: #5b8ca7; 193 color: #5b8ca7;
95} 194}
96 195
97/*------------------headlines-------------------*/
98
99h2 { 196h2 {
100 font-size: 1.5rem; 197 font-size: 1.5rem;
101 font-family: Helvetica, Arial, sans-serif; 198 font-family: Helvetica, Arial, sans-serif;
102 line-height: 1.75rem; 199 line-height: 1.75rem;
103} 200}
104 201
105div#center_column h2 a { 202h3 {
106 color: CanvasText; 203 font-size: 1.3rem;
107 text-decoration: none; 204 text-decoration: none;
205
108} 206}
109 207
110div#center_column h2 a:hover { 208h4 {
111 color: color-mix(in srgb, CanvasText, #808080 50%); 209 font-size: 1.0rem;
210 text-decoration: none;
112} 211}
113 212
114div#center_column h2.headline { 213h1, h2, h3 {
115 margin-top: 10px; 214 word-wrap: anywhere;
215 hyphens:auto;
116} 216}
117 217
118div#center_column .article_partial h2.headline { 218/* ==========================================================================
119 padding-top: 30px; 219 5. LAYOUT GRID
120 margin-bottom: 0.3rem; 220 ==========================================================================
121 border-top: 2px solid #cccccc; 221 ASSUMES: the full token lattice; header.png (drawn width = --page-width,
222 height = --header-height); left_column.png (continues the header art
223 down the left band via the ::before at z-index -10).
224 KNOWN STRUCTURAL DEBT: all three columns are position:absolute on
225 desktop, but #footer lives INSIDE #center_column — a long sidebar can
226 overflow past the footer. The column-height assumption is nowhere
227 enforced.
228 MOBILE ANCHORING: on max-width, #wrapper has no position, so the
229 absolutely positioned toolbox children (#burger-div, #search,
230 #light-mode-div) anchor to the viewport, not to #toolbox.
231 ========================================================================== */
232
233@media(min-width:1016px) {
234 div#wrapper {
235 position: relative;
236 width: var(--page-width); /* header.png's drawn width */
237 margin: 0 auto 0 auto;
238 text-align: left;
239 min-height: 100%;
240 }
241 .break-mobile {
242 display: block;
243 }
122} 244}
123 245
124/* except for erfa list */ 246/* Header */
125div#center_column .chapter_partial h2.headline { 247#header img,
126 border-top: none; 248#header > a {
249 display: block;
250 line-height: 0;
127} 251}
128 252
129div.article_partial h2 a { 253div#header img {
130 text-decoration: none; 254 border: none;
131} 255}
132 256
133h3 { 257#left_column {
134 font-size: 1.3rem; 258 line-height: 1.5em;
135 text-decoration: none; 259}
260
261@media(min-width:1016px) {
262 div#left_column::before {
263 content: " ";
264 position: absolute;
265 left: 0;
266 top: 0;
267 width: 100%;
268 height: 100%;
269 box-sizing: border-box;
270 background-image: url(/images/left_column.png);
271 background-position: top right;
272 background-repeat: no-repeat;
273 z-index: -10;
274 }
275 div#left_column {
276 position: absolute;
277 left: 0px;
278 width: var(--left-col-width);
279 min-height: 100px;
280 text-align: right;
281 padding-right: var(--left-col-pad); /* left band total: 185px */
282 }
136 283
284 div#left_column > a {
285 font-size: 0.8rem;
286 }
137} 287}
138 288
139h4 { 289div#center_column {
140 font-size: 1.0rem; 290 padding-bottom: 40px;
141 text-decoration: none;
142} 291}
143 292
144#left_column { 293@media(max-width: 1015px) {
145 line-height: 1.5em; 294div#center_column {
295 padding: 0 15px 40px 15px;
296}
146} 297}
147 298
148/*------------------main-navigation-------------------*/ 299@media(min-width: 1016px) {
300 div#center_column {
301 position: absolute;
302 background-color: Canvas;
303 left: var(--center-left);
304 width: var(--center-width);
305 }
306}
307
308@media(min-width:1016px) {
309 div#right_column {
310 position: absolute;
311 background-color: Canvas;
312 padding-left: var(--right-pad);
313 left: var(--right-left); /* = center-left + center-width */
314 width: var(--right-width); /* right band total: 210px; 690 + 210 = 900,
315 9px short of --page-width, absorbed
316 silently */
317 height: 100px;
318 }
319}
320
321/* Mobile toolbar strip. */
322@media(max-width:1015px) {
323#toolbox {
324 height: 30px;
325}
326}
327
328/* Footer */
329#footer {
330 border-bottom: 2px solid #aeadad;
331 border-top: 2px solid #aeadad;
332 bottom: 0;
333 color: CanvasText;
334}
335
336#footer p {
337 margin: .5rem auto;
338}
339
340#footer a {
341 margin-left: 1rem;
342 margin-right: 1rem;
343 color: CanvasText;
344}
345
346/* ==========================================================================
347 6. MAIN NAVIGATION & BURGER MENU
348 ==========================================================================
349 ASSUMES: --burger-right; the checkbox-hack markup order in the layout
350 (#toolbox precedes #left_column — the `#toolbox ~ #left_column`
351 sibling selectors break if the layout reorders them); left_column.png
352 behind the desktop nav. Mobile menu is pure CSS: .menu-checkbox state
353 drives both the burger-to-X morph and the menu slide.
354 COLLISION SET (order preserved): div.main_navigation ul base rule
355 before its min-width override — the override's `padding: 0` must win.
356 ========================================================================== */
149 357
150div.main_navigation ul { 358div.main_navigation ul {
151 margin-left: 0; 359 margin-left: 0;
152 padding-left: 0; 360 padding-left: 15px; /* dead duplicate `padding-left: 0` removed */
153 padding-left: 15px;
154 padding-right: 15px; 361 padding-right: 15px;
155 text-align: left; 362 text-align: left;
156} 363}
157 364
158 365
159@media(max-width:1016px) { 366@media(max-width:1015px) {
160 div.main_navigation li:not(:first-child):before { 367 div.main_navigation li:not(:first-child):before {
161 content: '•'; 368 content: '•';
162 margin-left: .3rem; 369 margin-left: .3rem;
@@ -203,7 +410,125 @@ div.main_navigation a.inactive, div.main_navigation span.inactive, div#left_colu
203 color: color-mix(in srgb, CanvasText, #808080 25%); 410 color: color-mix(in srgb, CanvasText, #808080 25%);
204} 411}
205 412
206/*------------------calendar-featured-tags-------------------*/ 413.menu-checkbox {
414 display: none;
415}
416
417.burger-menu {
418 display: none;
419 cursor: pointer;
420}
421
422#burger-div {
423 position: absolute;
424 right: var(--burger-right); /* anchors to the viewport on mobile,
425 see section 5 */
426}
427
428/* Mobile styles */
429@media (max-width: 1015px) {
430 .burger-menu {
431 position: absolute;
432 display: flex;
433 flex-direction: column;
434 transition: transform 0.3s ease, opacity 0.3s ease;
435 }
436
437 .burger-menu span {
438 display: inline-flex;
439 width: 26px;
440 height: 4px;
441 background: color-mix(in srgb, CanvasText, #808080 25%);
442 border-radius: 2px;
443 margin: 3px 0;
444 transition: max-height 0.5s ease-in-out, opacity 0.5s ease-in-out, transform 0.5s ease-in-out;
445 }
446
447 #left_column .main_navigation {
448 width: 100%;
449 position: relative;
450 }
451
452 #toolbox ~ #left_column .main_navigation ul {
453 transition: max-height 0.2s ease-in-out, transform 0.4s ease-in-out, opacity 0.4s ease;
454 overflow: hidden;
455 text-align: center;
456 }
457
458 #toolbox ~ #left_column .main_navigation:first-of-type ul {
459 transform: translateY(-200px);
460 max-height: 0;
461 opacity: 0;
462 z-index: -10;
463 }
464
465 /* Show menu when checkbox is checked */
466 #toolbox:has(.menu-checkbox:checked) ~ #left_column .main_navigation ul {
467 max-height: 400px;
468 opacity: 1;
469 transform: translateY(0);
470 }
471
472 .menu-checkbox:checked + .burger-menu span:nth-child(1) { transform: translateY(10px) rotate(45deg); }
473 .menu-checkbox:checked + .burger-menu span:nth-child(2) { opacity: 0; }
474 .menu-checkbox:checked + .burger-menu span:nth-child(3) { transform: translateY(-10px) rotate(-45deg); }
475}
476
477/* ==========================================================================
478 7. SEARCH
479 ==========================================================================
480 ASSUMES: --header-height, --search-left (desktop box hangs from
481 header.png's bottom edge); viewport anchoring on mobile (section 5).
482 ========================================================================== */
483
484/* Search bar */
485@media(min-width:1016px) {
486 div#search {
487 position: absolute;
488 top: var(--header-height); /* bottom edge of header.png */
489 left: var(--search-left);
490 height: 25px;
491 vertical-align: top;
492 }
493}
494
495@media(max-width:1015px) {
496 div#search {
497 position: absolute;
498 left: 25px; /* mobile toolbar offset; anchors to the viewport */
499 height: 25px;
500 }
501}
502
503div#search input {
504 color: CanvasText !important;
505}
506div#search input[type=search],
507div#search input[type=text] {
508 display: block;
509 padding: 2px;
510 margin: 0px;
511 height: 25px;
512 width: 132px;
513 line-height: 20px;
514 border: solid #808080 1px;
515 background-color: Canvas;
516 border-radius: 5px;
517 margin-right: 5px;
518 text-indent: 0.5rem;
519}
520
521/* ==========================================================================
522 8. SIDEBAR WIDGETS (calendar, tags, featured, open erfas)
523 ==========================================================================
524 ASSUMES: right-column geometry from section 5; div.main_navigation h2
525 also picks up the grouped border rule below.
526 COLLISION SET (order preserved verbatim): the grouped h2 border rule,
527 then the same-specificity div#frontpage_calendar h2 override
528 (border-top: none), then the min-width block that re-adds a border-top
529 for desktop. Moving the override above the group rule would grow a
530 border on the calendar heading.
531 ========================================================================== */
207 532
208div#frontpage_calendar { 533div#frontpage_calendar {
209 display: none; 534 display: none;
@@ -246,7 +571,7 @@ div#open_erfas_today .event_time {
246 color: color-mix(in srgb, CanvasText, #808080); 571 color: color-mix(in srgb, CanvasText, #808080);
247} 572}
248 573
249@media(max-width:1016px) { 574@media(max-width:1015px) {
250 div#tags li { 575 div#tags li {
251 list-style-type: none; 576 list-style-type: none;
252 display: inline-block; 577 display: inline-block;
@@ -278,6 +603,8 @@ div#frontpage_calendar ul, div#tags ul, div#featured_articles ul, div#open_erfas
278 line-height: 1.5em; 603 line-height: 1.5em;
279} 604}
280 605
606/* ODDITY, kept: per-widget icon paddings — delete when these icons rotate
607 out of the featured box */
281div#featured_articles #ds_icon img { 608div#featured_articles #ds_icon img {
282 padding-top: 10px; 609 padding-top: 10px;
283} 610}
@@ -312,302 +639,74 @@ div#featured_articles img {
312 filter: grayscale(1); 639 filter: grayscale(1);
313} 640}
314 641
315dt { 642#tags ul li {
316 font-weight: bold; 643 margin-block-start: 0.5rem;
317 margin-bottom: 1em; 644 margin-block-end: 0.5rem;
318}
319
320dd {
321 margin-bottom: 1em;
322}
323
324/*--------------------------------------------------------------*/
325
326div.author_and_date {
327 font-style: italic;
328 font-family: Georgia;
329 color: color-mix(in srgb, CanvasText, #808080);
330 padding-top: .2rem;
331 padding-bottom: .8rem;
332}
333
334@media(min-width:1016px) {
335 div#left_column::before {
336 content: " ";
337 position: absolute;
338 left: 0;
339 top: 0;
340 width: 100%;
341 height: 100%;
342 box-sizing: border-box;
343 background-image: url(/images/left_column.png);
344 background-position: top right;
345 background-repeat: no-repeat;
346 z-index: -10;
347 }
348 div#left_column {
349 position: absolute;
350 left: 0px;
351 width: 135px;
352 min-height: 100px;
353 text-align: right;
354 padding-right: 50px;
355 }
356
357 div#left_column > a {
358 font-size: 0.8rem;
359 }
360}
361
362.menu-checkbox {
363 display: none;
364}
365
366.burger-menu {
367 display: none;
368 cursor: pointer;
369}
370
371#burger-div {
372 position: absolute;
373 right: 54px;
374}
375
376/* Mobile styles */
377@media (max-width: 1016px) {
378 .burger-menu {
379 position: absolute;
380 display: flex;
381 flex-direction: column;
382 transition: transform 0.3s ease, opacity 0.3s ease;
383 }
384
385 .burger-menu span {
386 display: inline-flex;
387 width: 26px;
388 height: 4px;
389 background: color-mix(in srgb, CanvasText, #808080 25%);
390 border-radius: 2px;
391 margin: 3px 0;
392 transition: max-height 0.5s ease-in-out, opacity 0.5s ease-in-out, transform 0.5s ease-in-out;
393 }
394
395 #left_column .main_navigation {
396 width: 100%;
397 position: relative;
398 }
399
400 #toolbox ~ #left_column .main_navigation ul {
401 transition: max-height 0.2s ease-in-out, transform 0.4s ease-in-out, opacity 0.4s ease;
402 overflow: hidden;
403 text-align: center;
404 }
405
406 #toolbox ~ #left_column .main_navigation:first-of-type ul {
407 transform: translateY(-200px);
408 max-height: 0;
409 opacity: 0;
410 z-index: -10;
411 }
412
413 /* Show menu when checkbox is checked */
414 #toolbox:has(.menu-checkbox:checked) ~ #left_column .main_navigation ul {
415 max-height: 400px;
416 opacity: 1;
417 transform: translateY(0);
418 }
419
420 .menu-checkbox:checked + .burger-menu span:nth-child(1) { transform: translateY(10px) rotate(45deg); }
421 .menu-checkbox:checked + .burger-menu span:nth-child(2) { opacity: 0; }
422 .menu-checkbox:checked + .burger-menu span:nth-child(3) { transform: translateY(-10px) rotate(-45deg); }
423}
424
425div#center_column {
426 padding-bottom: 40px;
427}
428
429@media(max-width: 1016px) {
430div#center_column {
431 padding: 0 15px 40px 15px;
432}
433}
434
435@media(min-width: 1016px) {
436 div#center_column {
437 position: absolute;
438 background-color: Canvas;
439 left: 200px;
440 width: 490px;
441 }
442}
443
444@media(min-width:1016px) {
445 div#right_column {
446 position: absolute;
447 background-color: Canvas;
448 padding-left: 55px;
449 left: 690px;
450 width: 155px;
451 height: 100px;
452 }
453}
454
455div.teaser_ruler {
456 border-top: 1px solid #CCCCCC;
457 border-bottom: 1px dashed #CCCCCC;
458 height: 3px;
459 margin-top: 30px;
460}
461
462div.article_partial {
463 margin-bottom: 30px;
464}
465
466
467div.article_partial p.excerpt {
468 color: CanvasText;
469}
470
471/* Search bar */
472@media(min-width:1016px) {
473 div#search {
474 position: absolute;
475 top: 145px;
476 left: 676px;
477 height: 25px;
478 vertical-align: top;
479 }
480}
481
482@media(max-width:1016px) {
483 div#search {
484 position: absolute;
485 left: 25px;
486 height: 25px;
487 }
488}
489
490div#search input {
491 color: CanvasText !important;
492}
493div#search input[type=search],
494div#search input[type=text] {
495 display: block;
496 padding: 2px;
497 margin: 0px;
498 height: 25px;
499 width: 132px;
500 line-height: 20px;
501 border: solid #808080 1px;
502 background-color: Canvas;
503 border-radius: 5px;
504 margin-right: 5px;
505 text-indent: 0.5rem;
506} 645}
507 646
508/* Header */ 647/* ==========================================================================
509#header img, 648 9. ARTICLE CONTENT
510#header > a { 649 ==========================================================================
511 display: block; 650 ASSUMES: center-column geometry from section 5; TinyMCE emits the
512 line-height: 0; 651 inline-image classes (style-src unsafe-inline in the CSP exists partly
513} 652 for its img[style] output).
653 ========================================================================== */
514 654
515/* Main section */ 655/* Main section */
516.article, .article_partial { 656.article, .article_partial {
517 text-align: left; 657 text-align: left;
518} 658}
519 659
520h1, h2, h3 { 660div#center_column h2 a {
521 word-wrap: anywhere;
522 hyphens:auto;
523}
524
525.pagination {
526 margin-bottom: .5rem;
527}
528
529li {
530 line-height: 1.5rem;
531 margin-block-start: 1rem;
532 margin-block-end: 1rem;
533}
534
535#tags ul li {
536 margin-block-start: 0.5rem;
537 margin-block-end: 0.5rem;
538}
539
540/* Footer */
541#footer {
542 border-bottom: 2px solid #aeadad;
543 border-top: 2px solid #aeadad;
544 bottom: 0;
545 color: CanvasText; 661 color: CanvasText;
662 text-decoration: none;
546} 663}
547 664
548#footer > br { 665div#center_column h2 a:hover {
549 display: none; 666 color: color-mix(in srgb, CanvasText, #808080 50%);
550} 667}
551 668
552#footer p { 669div#center_column h2.headline {
553 margin: .5rem auto; 670 margin-top: 10px;
554} 671}
555 672
556#footer a { 673div#center_column .article_partial h2.headline {
557 margin-left: 1rem; 674 padding-top: 30px;
558 margin-right: 1rem; 675 margin-bottom: 0.3rem;
559 color: CanvasText; 676 border-top: 2px solid #cccccc;
560} 677}
561 678
562@media(max-width:1016px) { 679/* except for erfa list */
563#toolbox { 680div#center_column .chapter_partial h2.headline {
564 display: relative; 681 border-top: none;
565 height: 30px;
566}
567} 682}
568 683
569/* Light and dark mode button magic */ 684div.article_partial h2 a {
570@media(max-width:1016px) { 685 text-decoration: none;
571 div#light-mode-div {
572 position: absolute;
573 left: 170px;
574 }
575} 686}
576 687
577@media(min-width:1016px) { 688div.article_partial {
578 div#light-mode-div { 689 margin-bottom: 30px;
579 position: absolute;
580 top: 145px;
581 left: 816px;
582 }
583}
584input#light-mode[type="checkbox"] {
585 display: none;
586} 690}
587 691
588label[for=light-mode] {
589 font-size: 25px;
590 user-select: none;
591 cursor: pointer;
592 filter: grayscale(1) contrast(10%);
593}
594 692
595.hide-me { 693div.article_partial p.excerpt {
596 display: none; 694 color: CanvasText;
597} 695}
598 696
599/* Temporary glowing style for easterhegg 22 */ 697div.author_and_date {
600#eh22_icon img { 698 font-style: italic;
601 animation: animate 3s linear infinite; 699 font-family: Georgia;
700 color: color-mix(in srgb, CanvasText, #808080);
701 padding-top: .2rem;
702 padding-bottom: .8rem;
602} 703}
603 704
604@keyframes animate { 705div.teaser_ruler {
605 from { 706 border-top: 1px solid #CCCCCC;
606 filter: sepia(100%) hue-rotate(0deg); 707 border-bottom: 1px dashed #CCCCCC;
607 } 708 height: 3px;
608 to { 709 margin-top: 30px;
609 filter: sepia(100%) hue-rotate(360deg);
610 }
611} 710}
612 711
613.chapter_partial_layout { 712.chapter_partial_layout {
diff --git a/public/stylesheets/ccc.css_ b/public/stylesheets/ccc.css_
deleted file mode 100644
index 9c5dbba..0000000
--- a/public/stylesheets/ccc.css_
+++ /dev/null
@@ -1,579 +0,0 @@
1html {
2 height: 100%;
3 line-height: 1.6rem;
4}
5
6body {
7 margin: 0;
8 padding: 0;
9 text-align: center;
10 font-family: Verdana, Helvetica, Arial, sans-serif;
11 background-color: Canvas;
12 color: color-mix(in srgb, CanvasText, #808080 25%);
13 hyphens: auto;
14 color-scheme: light dark;
15
16 min-height: 100%;
17 height: 100%;
18}
19
20@media (prefers-color-scheme: light) {
21 body:has(#light-mode:checked) {
22 color-scheme: dark;
23 }
24 #light-mode + label[for=light-mode]:before { content: '🌙'; }
25 #light-mode:checked + label[for=light-mode]:before { content: '☀️'; }
26}
27
28@media (prefers-color-scheme: dark) {
29 body:has(#light-mode:checked) {
30 color-scheme: light;
31 }
32 #light-mode + label[for=light-mode]:before { content: '☀️'; }
33 #light-mode:checked + label[for=light-mode]:before { content: '🌙'; }
34
35 /* The header images were set in the before times. Let them shine in dark
36 mode, too */
37 body:not(:has(#light-mode:checked)) #header img,
38 body:not(:has(#light-mode:checked)) div#left_column::before
39 {
40 filter: invert(50%);
41 }
42}
43
44img {
45 max-width: 100%;
46}
47
48div#header img {
49 border: none;
50}
51
52@media(min-width:1016px) {
53 div#wrapper {
54 position: relative;
55 width: 909px;
56 margin: 0 auto 0 auto;
57 text-align: left;
58 min-height: 100%;
59 }
60 .break-mobile {
61 display: block;
62 }
63}
64
65pre {
66 overflow: auto;
67}
68
69.right {
70 text-align: right;
71}
72
73/*------------------links-------------------*/
74
75a {
76 color: #F8921E;
77 text-decoration: none;
78}
79
80a:visited {
81 color: #D1791A;
82 text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
83}
84
85a:hover {
86 color: #5b8ca7;
87}
88
89/*------------------headlines-------------------*/
90
91h2 {
92 font-size: 1.5rem;
93 font-family: Helvetica, Arial, sans-serif;
94 line-height: 1.75rem;
95}
96
97div#center_column h2 a {
98 color: CanvasText;
99 text-decoration: none;
100}
101
102div#center_column h2 a:hover {
103 color: color-mix(in srgb, CanvasText, #808080 50%);
104}
105
106div#center_column h2.headline {
107 margin-top: 10px;
108}
109
110div#center_column .article_partial h2.headline {
111 padding-top: 30px;
112 margin-bottom: 0.3rem;
113 border-top: 2px solid #cccccc;
114}
115
116div.article_partial h2 a {
117 text-decoration: none;
118}
119
120h3 {
121 font-size: 1.3rem;
122 text-decoration: none;
123
124}
125
126h4 {
127 font-size: 1.0rem;
128 text-decoration: none;
129}
130
131#left_column {
132 line-height: 1.5em;
133}
134
135/*------------------main-navigation-------------------*/
136
137div.main_navigation ul {
138 margin-left: 0;
139 padding-left: 0;
140 padding-left: 15px;
141 padding-right: 15px;
142 text-align: left;
143}
144
145
146@media(max-width:1016px) {
147 div.main_navigation li:not(:first-child):before {
148 content: '•';
149 margin-left: .3rem;
150 margin-right: .6rem;
151 }
152
153 div.main_navigation li {
154 list-style-type: none;
155 display: inline-block;
156 line-height: .75rem;
157 }
158}
159
160@media(min-width:1016px) {
161 div.main_navigation ul {
162 padding: 0;
163 text-align: right;
164 }
165
166 div.main_navigation li {
167 list-style-type: none;
168 line-height: 1.45em;
169 }
170}
171
172div.main_navigation a {
173 text-decoration: none;
174}
175
176div.main_navigation a.inactive:hover, div#left_column span.inactive:hover {
177 color: color-mix(in srgb, CanvasText, #808080 50%);
178}
179
180div.main_navigation a.active:before {
181 content: "▸";
182}
183
184div.main_navigation a.active {
185 color: CanvasText;
186 text-decoration: none;
187}
188
189div.main_navigation a.inactive, div.main_navigation span.inactive, div#left_column span.inactive {
190 color: color-mix(in srgb, CanvasText, #808080 25%);
191}
192
193/*------------------calendar-featured-tags-------------------*/
194
195div#frontpage_calendar {
196 display: none;
197 margin-top: 10px;
198}
199
200@media(min-width:1016px) {
201 div#frontpage_calendar {
202 margin-top: 30px;
203 }
204}
205
206div#frontpage_calendar h2, div#tags h2, div#featured_articles h2, div.main_navigation h2 {
207 border-top: 2px solid;
208 border-bottom: 2px solid;
209 font-size: 1.1em;
210 padding-top: 2px;
211 padding-bottom: 2px;
212}
213
214div#frontpage_calendar h2 {
215 display: none;
216 border-top: none;
217 margin-left: auto;
218 margin-right: auto;
219 padding-left: 0.5em;
220 padding-right: 0.5em;
221 margin-top: 0;
222}
223
224@media(max-width:1016px) {
225 div#tags li {
226 list-style-type: none;
227 display: inline-block;
228 }
229
230 div#tags li:not(:first-child):before {
231 content: '•';
232 margin-left: .3em;
233 margin-right: .6em;
234 }
235}
236
237@media(min-width:1016px) {
238 div#frontpage_calendar h2, div#tags h2, div#featured_articles h2 {
239 font-size: 1rem;
240 }
241
242 div#frontpage_calendar h2 {
243 display: block;
244 border-top: 2px solid #aeadad;
245 padding: 2px 0;
246 }
247
248}
249
250div#frontpage_calendar ul, div#tags ul, div#featured_articles ul {
251 padding: 0px;
252 font-size: 1rem;
253 line-height: 1.5em;
254}
255
256div#featured_articles #ds_icon img {
257 padding-top: 10px;
258}
259
260div#featured_articles #events_icon img {
261 padding-left: 10px;
262}
263
264div#frontpage_calendar li {
265 margin-bottom: 20px;
266}
267
268div#frontpage_calendar li, div#tags li, div#featured_articles li {
269 list-style-type: none;
270}
271
272div#frontpage_calendar li a, div#tags li a, div#featured_articles li a {
273 text-decoration: none;
274 color: color-mix(in srgb, CanvasText, #808080 25%);
275}
276
277div#frontpage_calendar li a:hover,
278div#tags li a:hover,
279div#featured_articles li a:hover {
280 text-decoration: none;
281 color: color-mix(in srgb, CanvasText, #808080 50%);
282}
283
284/* We don't want the only colourful thing on the site to be pointing
285 * somewhere else */
286div#featured_articles img {
287 filter: grayscale(1);
288}
289
290dt {
291 font-weight: bold;
292 margin-bottom: 1em;
293}
294
295dd {
296 margin-bottom: 1em;
297}
298
299/*--------------------------------------------------------------*/
300
301div.author_and_date {
302 font-style: italic;
303 font-family: Georgia;
304 color: color-mix(in srgb, CanvasText, #808080);
305 padding-top: 0;
306 padding-bottom: 1rem;
307}
308
309@media(min-width:1016px) {
310 div#left_column::before {
311 content: " ";
312 position: absolute;
313 left: 0;
314 top: 0;
315 width: 100%;
316 height: 100%;
317 box-sizing: border-box;
318 background-image: url(/images/left_column.png);
319 background-position: top right;
320 background-repeat: no-repeat;
321 z-index: -10;
322 }
323 div#left_column {
324 position: absolute;
325 left: 0px;
326 width: 135px;
327 min-height: 100px;
328 text-align: right;
329 padding-right: 50px;
330 }
331
332 div#left_column > a {
333 font-size: 0.8rem;
334 }
335}
336
337.menu-checkbox {
338 display: none;
339}
340
341.burger-menu {
342 display: none;
343 cursor: pointer;
344}
345
346/* Mobile styles */
347@media (max-width: 1016px) {
348 .main_navigation {
349 width: 100%;
350 position: relative;
351 }
352
353 .main_navigation ul {
354 transition: max-height 0.5s ease-in-out, transform .4s ease, opacity 0.4s ease;
355 overflow: hidden;
356 text-align: center;
357 }
358
359
360 .burger-menu {
361 position: absolute;
362 top: -48px;
363 right: 30px;
364 display: flex;
365 flex-direction: column;
366 transition: transform 0.3s ease, opacity 0.3s ease;
367 }
368
369 .burger-menu span {
370 display: inline-flex;
371 width: 30px;
372 height: 4px;
373 background: color-mix(in srgb, CanvasText, #808080 25%);
374 border-radius: 2px;
375 margin: 3px 0;
376 transition: max-height 0.5s ease-in-out, opacity 0.5s ease-in-out, transform 0.5s ease-in-out;
377 }
378 .main_navigation .menu-checkbox:not(:checked) ~ ul {
379 transform: translateY(-60px);
380 max-height: 0;
381 opacity: 0;
382 }
383
384 /* Show menu when checkbox is checked */
385 .main_navigation .menu-checkbox ~ ul {
386 max-height: 400px;
387 opacity: 1;
388 transform: translateY(0);
389 text-align: center;
390 }
391
392 .menu-checkbox:checked + .burger-menu span:nth-child(1) { transform: translateY(10px) rotate(45deg); }
393 .menu-checkbox:checked + .burger-menu span:nth-child(2) { opacity: 0; }
394 .menu-checkbox:checked + .burger-menu span:nth-child(3) { transform: translateY(-10px) rotate(-45deg); }
395}
396
397div#center_column {
398 padding-bottom: 40px;
399}
400
401@media(max-width: 1016px) {
402div#center_column {
403 padding: 0 15px 40px 15px;
404}
405}
406
407@media(min-width: 1016px) {
408 div#center_column {
409 position: absolute;
410 background-color: Canvas;
411 left: 200px;
412 width: 490px;
413 }
414}
415
416@media(min-width:1016px) {
417 div#right_column {
418 position: absolute;
419 background-color: Canvas;
420 padding-left: 55px;
421 left: 690px;
422 width: 155px;
423 height: 100px;
424 }
425}
426
427div.teaser_ruler {
428 border-top: 1px solid #CCCCCC;
429 border-bottom: 1px dashed #CCCCCC;
430 height: 3px;
431 margin-top: 30px;
432}
433
434div.article_partial {
435 margin-bottom: 30px;
436}
437
438
439div.article_partial p.excerpt {
440 color: CanvasText;
441}
442
443/* Search bar */
444@media(min-width:1016px) {
445 div#search {
446 position: absolute;
447 top: 145px;
448 left: 676px;
449 height: 25px;
450 vertical-align: top;
451 }
452}
453
454@media(max-width:1016px) {
455 div#search {
456 position: relative;
457 bottom: 5px;
458 left: 0px;
459 margin-left: 25px;
460 height: 25px;
461 vertical-align: top;
462 }
463}
464
465div#search input {
466 color: CanvasText !important;
467}
468div#search input[type=search],
469div#search input[type=text] {
470 display: block;
471 padding: 2px;
472 margin: 0px;
473 height: 25px;
474 width: 132px;
475 line-height: 20px;
476 border: solid #808080 1px;
477 background-color: Canvas;
478 border-radius: 5px;
479 margin-right: 5px;
480 text-indent: 0.5rem;
481}
482
483/* Header */
484#header img,
485#header > a {
486 display: block;
487 line-height: 0;
488}
489
490/* Main section */
491.article, .article_partial {
492 text-align: left;
493}
494
495h1, h2, h3 {
496 word-wrap: anywhere;
497 hyphens:auto;
498}
499
500.pagination {
501 margin-bottom: .5rem;
502}
503
504li {
505 line-height: 1.5rem;
506 margin-block-start: 1rem;
507 margin-block-end: 1rem;
508}
509
510#tags ul li {
511 margin-block-start: 0.5rem;
512 margin-block-end: 0.5rem;
513}
514
515/* Footer */
516#footer {
517 border-bottom: 2px solid #aeadad;
518 border-top: 2px solid #aeadad;
519 bottom: 0;
520 color: CanvasText;
521}
522
523#footer > br {
524 display: none;
525}
526
527#footer p {
528 margin: .5rem auto;
529}
530
531#footer a {
532 margin-left: 1rem;
533 margin-right: 1rem;
534 color: CanvasText;
535}
536
537/* Light and dark mode button magic */
538@media(max-width:1016px) {
539 div#light-mode-li {
540 position: absolute;
541 top: 145px;
542 left: 25px;
543 }
544}
545
546@media(min-width:1016px) {
547 div#light-mode-li {
548 position: absolute;
549 top: 145px;
550 left: 816px;
551 }
552}
553input#light-mode[type="checkbox"] {
554 display: none;
555}
556
557label[for=light-mode] {
558 font-size: 25px;
559 user-select: none;
560 filter: grayscale(1);
561}
562
563.hide-me {
564 display: none;
565}
566
567/* Temporary glowing style for easterhegg 22 */
568#eh22_icon img {
569 animation: animate 3s linear infinite;
570}
571
572@keyframes animate {
573 from {
574 filter: sepia(100%) hue-rotate(0deg);
575 }
576 to {
577 filter: sepia(100%) hue-rotate(360deg);
578 }
579}
diff --git a/public/stylesheets/ccc.css_old b/public/stylesheets/ccc.css_old
deleted file mode 100644
index 93651b9..0000000
--- a/public/stylesheets/ccc.css_old
+++ /dev/null
@@ -1,257 +0,0 @@
1body {
2 background-color: #FFFFFF;
3 margin: 0;
4 padding: 0;
5 text-align: center;
6 font-family: Verdana, Helvetica, Arial, sans-serif;
7 font-size: 11px;
8 line-height: 16px;
9 background-color: #ffffff;
10 color: #535353;
11}
12
13div#header {
14 height: 224px;
15}
16
17div#header img {
18 border: none;
19}
20
21div#wrapper {
22 position: relative;
23 width: 909px;
24 margin-left: 90px;
25 text-align: left;
26}
27
28pre {
29 overflow: auto;
30}
31
32.right {
33 text-align: right;
34}
35
36/*------------------links-------------------*/
37
38a {
39 color: #F8921E;
40 text-decoration: none;
41}
42
43a:visited {
44 color: #5b8ca7;
45}
46
47a:hover {
48 color: #5b8ca7;
49}
50
51/*------------------headlines-------------------*/
52
53h2 {
54 font-size: 15px;
55 line-height: 20px;
56 font-family: Helvetica;
57}
58
59div#center_column h2 a {
60 color: #535353;
61 text-decoration: none;
62}
63
64div#center_column h2 a:hover {
65 color: #8e8e8e;
66}
67
68div#center_column h2.headline {
69 margin-top: 10px;
70 padding-top: 30px;
71 border-top: 2px solid #cccccc;
72}
73
74div.article_partial h2 a {
75 text-decoration: none;
76}
77
78h3 {
79 font-size: 13px;
80 text-decoration: none;
81
82}
83
84h4 {
85 font-size: 11px;
86 text-decoration: bold;
87}
88
89/*------------------main-navigation-------------------*/
90
91div.main_navigation ul {
92 margin-left: 0;
93 padding-left: 0;
94 line-height: 18px;
95}
96
97div.main_navigation li {
98 list-style-type: none;
99}
100
101div.main_navigation a {
102 text-decoration: none;
103 font-size: 14px;
104}
105
106div.main_navigation a.inactive:hover {
107 color: #F8921E;
108}
109
110div.main_navigation a.active {
111 color: #000000;
112 text-decoration: none;
113 font-size: 13px;
114}
115
116div.main_navigation a.inactive {
117 color: #aeadad;
118 font-size: 13px;
119}
120
121/*------------------calendar-featured-tags-------------------*/
122
123div#frontpage_calendar {
124 margin-top: 30px;
125}
126
127div#frontpage_calendar h2, div#tags h2, div#featured_articles h2 {
128 color: #aeadad;
129 border-top: 2px solid #aeadad;
130 border-bottom: 2px solid #aeadad;
131 font-size: 16px;
132 padding-top: 2px;
133 padding-bottom: 2px;
134}
135
136div#frontpage_calendar ul, div#tags ul, div#featured_articles ul {
137 padding: 0px;
138 font-size: 11px;
139}
140
141div#featured_articles #ds_icon img {
142 padding-top: 10px;
143}
144
145div#featured_articles #events_icon img {
146 padding-left: 10px;
147}
148
149div#frontpage_calendar li {
150 margin-bottom: 20px;
151}
152
153div#frontpage_calendar li, div#tags li, div#featured_articles li {
154 list-style-type: none;
155}
156
157div#frontpage_calendar li a, div#tags li a, div#featured_articles li a {
158 text-decoration: none;
159 color: #535353;
160}
161
162div#frontpage_calendar li a:hover,
163div#tags li a:hover,
164div#featured_articles li a:hover {
165 text-decoration: none;
166 color: #ff9600;
167}
168
169/*--------------------------------------------------------------*/
170
171div.author_and_date {
172 font-style: italic;
173 padding-left: 15px;
174 font-family: Georgia;
175}
176
177div#left_column {
178 position: absolute;
179 background-image: url(/images/left_column.png);
180 background-position: top right;
181 background-repeat: no-repeat;
182 left: 0px;
183 width: 115px;
184 min-height: 100px;
185 text-align: right;
186 padding-right: 70px;
187}
188
189
190div#center_column {
191 position: absolute;
192 background-color: #ffffff;
193 left: 200px;
194 width: 460px;
195 padding-left: 15px;
196 padding-right: 15px;
197 padding-bottom: 40px;
198}
199
200div#right_column {
201 position: absolute;
202 background-color: #ffffff;
203 padding-left: 70px;
204 left: 675px;
205 width: 155px;
206 height: 100px;
207}
208
209div.teaser_ruler {
210 border-top: 1px solid #CCCCCC;
211 border-bottom: 1px dashed #CCCCCC;
212 height: 3px;
213 margin-top: 30px;
214}
215
216div.article_partial {
217 margin-bottom: 30px;
218}
219
220
221div.article_partial p.excerpt {
222 color: #404040;
223}
224
225div#center_column div.body,
226div#center_column div.abstract {
227 font-size: 11px;
228 line-height: 16px;
229}
230
231div#search {
232 position: absolute;
233 top: 145px;
234 left: 676px;
235 height: 20px;
236 vertical-align: top;
237}
238
239div#search input[type=button] {
240 display: block;
241 height: 20px;
242}
243
244div#search input[type=text] {
245 display: block;
246 padding: 0px;
247 margin: 0px;
248 height: 20px;
249 width: 132px;
250 line-height: 20px;
251 border: none;
252 background-image: url(/images/search_field.png);
253 background-repeat:no-repeat;
254 padding-right: 5px;
255 margin-right: 5px;
256 background-position: top top;
257}