decktile 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. decktile/__init__.py +5 -0
  2. decktile/__main__.py +3 -0
  3. decktile/assets/lucide/VENDORED.md +21 -0
  4. decktile/assets/lucide/lucide.css +2193 -0
  5. decktile/assets/lucide/lucide.ttf +3 -0
  6. decktile/cli.py +269 -0
  7. decktile/parser/__init__.py +0 -0
  8. decktile/parser/ast.py +191 -0
  9. decktile/parser/markdown.py +289 -0
  10. decktile/plugins/cards.py +25 -0
  11. decktile/plugins/directives.py +56 -0
  12. decktile/plugins/fenced.py +228 -0
  13. decktile/plugins/layouts.py +69 -0
  14. decktile/plugins/lucide.py +65 -0
  15. decktile/plugins/lucide_font.py +26 -0
  16. decktile/render/__init__.py +0 -0
  17. decktile/render/html.py +287 -0
  18. decktile/render/ir.py +536 -0
  19. decktile/render/odf/__init__.py +4 -0
  20. decktile/render/odf/geometry.py +510 -0
  21. decktile/render/odf/shapes.py +719 -0
  22. decktile/render/odf/styles.py +828 -0
  23. decktile/render/odp.py +380 -0
  24. decktile/render/pdf.py +17 -0
  25. decktile/render/pdg.py +72 -0
  26. decktile/render/seaborn.py +155 -0
  27. decktile/render/templates/base.css +741 -0
  28. decktile/render/templates/decktile.js +371 -0
  29. decktile/render/templates/shell.html +106 -0
  30. decktile/seaborn_styles/__init__.py +5 -0
  31. decktile/seaborn_styles/ossummit_europe.py +30 -0
  32. decktile/theme/__init__.py +0 -0
  33. decktile/theme/colors.py +53 -0
  34. decktile/theme/loader.py +22 -0
  35. decktile/theme/parser.py +264 -0
  36. decktile/themes/default.css +146 -0
  37. decktile/themes/kcd_vietnam.css +159 -0
  38. decktile/themes/kubecon_china.css +213 -0
  39. decktile/themes/kubecon_japan.css +156 -0
  40. decktile/themes/ossummit_europe.css +196 -0
  41. decktile/themes/ossummit_korea.css +196 -0
  42. decktile-0.2.0.dist-info/METADATA +625 -0
  43. decktile-0.2.0.dist-info/RECORD +46 -0
  44. decktile-0.2.0.dist-info/WHEEL +4 -0
  45. decktile-0.2.0.dist-info/entry_points.txt +6 -0
  46. decktile-0.2.0.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,264 @@
1
+ """Parse base.css + theme CSS with tinycss2 into PPTX styling rules."""
2
+
3
+ import tinycss2
4
+
5
+
6
+ def parse_theme(base_css: str, theme_css: str) -> dict:
7
+ """Parse base.css + theme CSS into a flat style map for ODP rendering."""
8
+ styles: dict[str, str] = {}
9
+ _parse_sheet(base_css, styles)
10
+ _parse_sheet(theme_css, styles)
11
+ return _build_theme_dict(styles)
12
+
13
+
14
+ def parse_dark_theme(base_css: str, theme_css: str) -> dict:
15
+ """Parse dark-mode CSS variables from [data-variant='dark'] blocks."""
16
+ import tinycss2
17
+ for sheet in [base_css, theme_css]:
18
+ rules = tinycss2.parse_stylesheet(sheet, skip_comments=True, skip_whitespace=True)
19
+ in_dark = False
20
+ for rule in rules:
21
+ if rule.type != "qualified-rule":
22
+ continue
23
+ selector = tinycss2.serialize(rule.prelude).strip()
24
+ if selector in ("[data-variant=\"dark\"]"):
25
+ in_dark = True
26
+ continue
27
+ elif selector.startswith("section[data-variant"):
28
+ in_dark = False
29
+ continue
30
+ if in_dark:
31
+ # Check if this is still inside a dark block or the start of another
32
+ pass
33
+ # Simpler: just concatenate all declarations from dark-mode blocks
34
+ # Simpler approach: extract -- variables from the dark blocks
35
+ styles: dict[str, str] = {}
36
+ _parse_sheet(base_css, styles)
37
+ _parse_sheet(theme_css, styles)
38
+ # Parse dark-specific CSS to get dark variable values
39
+ _parse_dark_variables(base_css, theme_css, styles)
40
+ return _build_theme_dict(styles)
41
+
42
+
43
+ def _parse_dark_variables(base_css: str, theme_css: str, styles: dict) -> None:
44
+ """Extract dark mode CSS variables and override light mode values."""
45
+ import tinycss2
46
+ for sheet in [base_css, theme_css]:
47
+ rules = tinycss2.parse_stylesheet(sheet, skip_comments=True, skip_whitespace=True)
48
+ for rule in rules:
49
+ if rule.type != "qualified-rule":
50
+ continue
51
+ selector = tinycss2.serialize(rule.prelude).strip()
52
+ if selector not in ("section[data-variant=\"dark\"]"):
53
+ continue
54
+ decls = tinycss2.parse_declaration_list(rule.content)
55
+ for decl in decls:
56
+ if decl.type != "declaration":
57
+ continue
58
+ name = decl.name
59
+ val = tinycss2.serialize(decl.value).strip()
60
+ if name.startswith("--"):
61
+ styles[name] = val
62
+
63
+
64
+
65
+ _parse_sheet(base_css, styles)
66
+ _parse_sheet(theme_css, styles)
67
+ # Also parse the dark mode overrides
68
+ def _extract_tag_colors(styles: dict) -> dict[str, tuple[str, str]]:
69
+ """Extract per-tag fill and border colors from CSS .tag-* selectors."""
70
+ import re
71
+ result: dict[str, list[str]] = {}
72
+
73
+ # From :root CSS variables
74
+ for key, val in styles.items():
75
+ m = re.match(r"--tag-(\w+)-(bg|border)", key)
76
+ if m:
77
+ tag, prop = m.group(1), m.group(2)
78
+ if tag not in result:
79
+ result[tag] = ["", ""]
80
+ result[tag][0 if prop == "bg" else 1] = _resolve_color_var(val, styles)
81
+
82
+ # From .tag-* selectors (fallback)
83
+ for key, val in styles.items():
84
+ m = re.match(r"tag_(\w+)_(border|background)", key)
85
+ if m:
86
+ tag, prop = m.group(1), m.group(2)
87
+ if tag not in result:
88
+ result[tag] = ["", ""]
89
+ result[tag][0 if prop == "background" else 1] = val
90
+
91
+ return {tag: (fill, border) for tag, (fill, border) in result.items()}
92
+
93
+
94
+ def _parse_sheet(css: str, styles: dict) -> None:
95
+ """Parse a CSS string and extract relevant property values into styles dict."""
96
+ rules = tinycss2.parse_stylesheet(css, skip_comments=True, skip_whitespace=True)
97
+
98
+ for rule in rules:
99
+ if rule.type != "qualified-rule":
100
+ continue
101
+
102
+ selector = tinycss2.serialize(rule.prelude).strip()
103
+ decls = tinycss2.parse_declaration_list(rule.content)
104
+
105
+ for decl in decls:
106
+ if decl.type != "declaration":
107
+ continue
108
+ name = decl.name
109
+ val = tinycss2.serialize(decl.value).strip()
110
+
111
+ # CSS variables
112
+ if selector == ":root" and name.startswith("--"):
113
+ styles[name] = val
114
+ continue
115
+
116
+ # Named selectors -> prefixed props
117
+ if val.startswith("var("):
118
+ continue
119
+ key = _prop_for(name, selector)
120
+ if key:
121
+ styles[key] = val
122
+
123
+ # Tag selectors (.tag-green, .tag-red, etc.)
124
+ import re
125
+ tm = re.match(r"\.tag-(\w+)", selector.strip())
126
+ if tm and name in ("background", "border-color"):
127
+ tag_key = f"tag_{tm.group(1)}_{'border' if name == 'border-color' else 'background'}"
128
+ styles[tag_key] = val
129
+
130
+
131
+ _PROPS = {
132
+ "section": {"padding": "section_padding", "font-family": "font_body_family", "text-align": "section_text_align", "font-size": "font_body_size"},
133
+ ".layout-title": {"text-align": "title_text_align"},
134
+ "code": {"font-family": "font_code_family", "font-size": "font_code_size"},
135
+ ".quote": {"font-size": "font_quote_size"},
136
+ ".kicker": {"font-size": "font_kicker_size"},
137
+ ".subtitle": {"font-size": "font_subtitle_size"},
138
+ ".speaker": {"font-size": "font_speaker_size"},
139
+ ".tiny": {"font-size": "font_tiny_size"},
140
+ }
141
+
142
+ _FONT_SIZES = {
143
+ "h1": "font_h1_size", "h2": "font_h2_size", "h3": "font_h3_size",
144
+ "h4": "font_h4_size", "h5": "font_h5_size", "h6": "font_h6_size",
145
+ "p": "font_body_size", "li": "font_li_size",
146
+ }
147
+
148
+ def _prop_for(name: str, selector: str) -> str | None:
149
+ if name == "font-size" and selector in _FONT_SIZES:
150
+ return _FONT_SIZES[selector]
151
+ return _PROPS.get(selector, {}).get(name)
152
+
153
+
154
+ def _to_rgb(value: str) -> tuple[int, int, int]:
155
+ value = value.strip()
156
+ if value.startswith("#"):
157
+ value = value.lstrip("#")
158
+ if len(value) == 3:
159
+ value = "".join(c * 2 for c in value)
160
+ if len(value) == 6:
161
+ return int(value[0:2], 16), int(value[2:4], 16), int(value[4:6], 16)
162
+ if value.startswith("rgb"):
163
+ import re
164
+ m = re.findall(r"\d+", value)
165
+ if len(m) >= 3:
166
+ return int(m[0]), int(m[1]), int(m[2])
167
+ return 0x33, 0x33, 0x33
168
+
169
+
170
+ def _parse_size(value: str, default: int) -> int:
171
+ """Parse a CSS size value to integer points."""
172
+ if not value or value.startswith("var("):
173
+ return default
174
+ value = value.strip().rstrip(";")
175
+ if value.endswith("px"):
176
+ return int(float(value[:-2]))
177
+ if value.endswith("pt"):
178
+ return int(float(value[:-2]))
179
+ if value.endswith("em"):
180
+ return int(float(value[:-2]) * 18)
181
+ return default
182
+
183
+
184
+ def _parse_padding(value: str) -> tuple[int, int, int, int]:
185
+ """Parse CSS padding into (top, right, bottom, left) in px."""
186
+ if not value:
187
+ return (50, 64, 50, 64)
188
+ parts = [p for p in value.split() if not p.startswith("var(")]
189
+ if not parts:
190
+ return (50, 64, 50, 64)
191
+ vals = [int(float(p.rstrip("pxem;"))) for p in parts]
192
+ if len(vals) == 1:
193
+ return (vals[0], vals[0], vals[0], vals[0])
194
+ if len(vals) == 2:
195
+ return (vals[0], vals[1], vals[0], vals[1])
196
+ if len(vals) == 4:
197
+ return (vals[0], vals[1], vals[2], vals[3])
198
+ return (50, 64, 50, 64)
199
+
200
+
201
+ def _parse_border_width(border: str) -> str:
202
+ """Extract border width from CSS border shorthand (e.g. '1px solid #ddd')."""
203
+ parts = border.split()
204
+ return parts[0] if parts else "1px"
205
+
206
+
207
+ def _parse_border_color(border: str) -> str:
208
+ """Extract border color from CSS border shorthand, after the style keyword."""
209
+ parts = border.split()
210
+ for i, p in enumerate(parts):
211
+ if p in ("solid", "dashed", "dotted", "double", "none"):
212
+ if i + 1 < len(parts):
213
+ return parts[i + 1]
214
+ return parts[-1] if parts else ""
215
+
216
+
217
+ def _resolve_color_var(color: str, styles: dict) -> str:
218
+ """Resolve var(--name) to its value from CSS variables."""
219
+ import re
220
+ m = re.match(r"var\((--[\w-]+)\)", color.strip())
221
+ if m:
222
+ return styles.get(m.group(1), color)
223
+ return color
224
+
225
+
226
+
227
+ def _build_theme_dict(styles: dict) -> dict:
228
+ ink = _to_rgb(styles.get("--ink", styles.get("--color-foreground", "")))
229
+ return {
230
+ "ink_rgb": ink,
231
+ "ink_rgb_hex": _rgb_to_hex(ink),
232
+ "muted_rgb": _to_rgb(styles.get("--muted", styles.get("--color-dimmed", ""))),
233
+ "accent_rgb": _to_rgb(styles.get("--accent", styles.get("--color-accent", ""))),
234
+ "font_body_family": styles.get("font_body_family", "Segoe UI"),
235
+ "font_code_family": styles.get("font_code_family",
236
+ styles.get("--font-mono", "Noto Sans Mono")),
237
+ "font_h1": _parse_size(styles.get("font_h1_size", ""), 32),
238
+ "font_h2": _parse_size(styles.get("font_h2_size", ""), 32),
239
+ "font_h3": _parse_size(styles.get("font_h3_size", ""), 23),
240
+ "font_h4": _parse_size(styles.get("font_h4_size", ""), 15),
241
+ "font_h5": _parse_size(styles.get("font_h5_size", ""), 13),
242
+ "font_h6": _parse_size(styles.get("font_h6_size", ""), 11),
243
+ "font_body": _parse_size(styles.get("font_body_size", ""), 18),
244
+ "font_code": _parse_size(styles.get("font_code_size", ""), 14),
245
+ "font_quote": _parse_size(styles.get("font_quote_size", ""), 24),
246
+ "font_kicker": _parse_size(styles.get("font_kicker_size", ""), 14),
247
+ "font_subtitle": _parse_size(styles.get("font_subtitle_size", ""), 32),
248
+ "font_speaker": _parse_size(styles.get("font_speaker_size", ""), 18),
249
+ "font_tiny": _parse_size(styles.get("font_tiny_size", ""), 13),
250
+ "font_li": _parse_size(styles.get("font_li_size", ""), 16),
251
+ "section_text_align": styles.get("section_text_align", "left"),
252
+ "title_text_align": styles.get("title_text_align", "left"),
253
+ "section_padding": _parse_padding(styles.get("section_padding", "")),
254
+ "border_radius": styles.get("--radius", "0.4em"),
255
+ "card_border_color": _resolve_color_var(
256
+ _parse_border_color(styles.get("--card-border", "")),
257
+ styles,
258
+ ),
259
+ "card_bg": styles.get("--color-card-bg", ""),
260
+ "tag_colors": _extract_tag_colors(styles),
261
+ }
262
+
263
+ def _rgb_to_hex(rgb: tuple) -> str:
264
+ return f"#{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}"
@@ -0,0 +1,146 @@
1
+ /* decktile default theme -- overridable via frontmatter style: block */
2
+
3
+ :root {
4
+ --color-background: #fff;
5
+ --color-background-stripe: #f5f5f5;
6
+ --color-foreground: #333;
7
+ --color-dimmed: #777;
8
+ --color-accent: #0288d1;
9
+ --color-accent-bg: rgba(2, 136, 209, 0.1);
10
+ --color-border: #ddd;
11
+ --color-card-bg: #fafafa;
12
+ --tag-green-bg: #e8f5e9;
13
+ --tag-cyan-bg: #e0f7fa;
14
+ --tag-yellow-bg: #fff9c4;
15
+ --tag-red-bg: #ffebee;
16
+ --tag-default-bg: #f5f5f5;
17
+ --tag-green-border: #0fd05d;
18
+ --tag-cyan-border: #67d8ff;
19
+ --tag-yellow-border: #ffd166;
20
+ --tag-red-border: #ff7a7a;
21
+ --tag-default-border: #ccc;
22
+ --list-style: "\25B8 ";
23
+ /* keep titles clear of the side-image QR: match kubecon sizing */
24
+ --title-h1-size: 2.5em;
25
+ --title-subtitle-size: 1.2em;
26
+ /* watermark size + clearance from the page-number column (both
27
+ anchored to --section-padding-h so they stay in sync) -- override
28
+ --watermark-w/-h per deck if the watermark image's own aspect
29
+ ratio needs it */
30
+ --watermark-w: 4em;
31
+ --watermark-h: 1em;
32
+ --watermark-gap: 1em;
33
+ }
34
+
35
+ /* per-slide variant override */
36
+ section[data-theme="light"] {
37
+ --color-background: #fff;
38
+ --color-background-stripe: #f5f5f5;
39
+ --color-foreground: #333;
40
+ --color-dimmed: #777;
41
+ --color-accent: #0288d1;
42
+ --color-accent-bg: rgba(2, 136, 209, 0.1);
43
+ --color-border: #ddd;
44
+ --color-card-bg: #fafafa;
45
+ --tag-green-bg: #e8f5e9;
46
+ --tag-cyan-bg: #e0f7fa;
47
+ --tag-yellow-bg: #fff9c4;
48
+ --tag-red-bg: #ffebee;
49
+ --tag-green-border: #0fd05d;
50
+ --tag-cyan-border: #67d8ff;
51
+ --tag-yellow-border: #ffd166;
52
+ --tag-red-border: #ff7a7a;
53
+ }
54
+ section[data-theme="dark"] {
55
+ --color-background: #1a1a2e;
56
+ --color-background-stripe: #16213e;
57
+ --color-foreground: #e0e0e0;
58
+ --color-dimmed: #999;
59
+ --color-accent: #4fc3f7;
60
+ --color-accent-bg: rgba(79, 195, 247, 0.15);
61
+ --color-border: #333;
62
+ --color-card-bg: #252535;
63
+ --tag-green-bg: #1a2e1a;
64
+ --tag-cyan-bg: #1a2a2e;
65
+ --tag-yellow-bg: #2e2a1a;
66
+ --tag-red-bg: #2e1a1a;
67
+ --tag-default-bg: #2a2a2a;
68
+ --tag-default-border: #444;
69
+ --tag-green-border: #2d8a3e;
70
+ --tag-cyan-border: #2d8aaa;
71
+ --tag-yellow-border: #8a7a2d;
72
+ --tag-red-border: #8a2d2d;
73
+ --tag-default-border: #444;
74
+ }
75
+
76
+ section {
77
+ background: var(--color-background);
78
+ color: var(--color-foreground);
79
+ font-family: Roboto, Arial, "Helvetica Neue", sans-serif;
80
+ }
81
+
82
+ /* Watermark sits left of, and vertically centered against, the page-
83
+ number column: anchored to --section-padding-h (same var .slide-num's
84
+ right offset uses) so the two stay clear of each other regardless of
85
+ watermark image size. bottom: 0.48em centers the watermark against
86
+ .slide-num's own 0.6em-font-size text box -- not the naive full 1em
87
+ (watermark sits too high) or a plain box-bottom match at 0.6em
88
+ (watermark sits slightly too high still, since the two boxes aren't
89
+ the same height). */
90
+ section::after {
91
+ right: calc(var(--section-padding-h) + var(--watermark-gap));
92
+ bottom: 0.48em;
93
+ width: var(--watermark-w);
94
+ height: var(--watermark-h);
95
+ background-position: center;
96
+ }
97
+
98
+ h1, h2, h3, h4, h5, h6 { color: var(--color-foreground); }
99
+ h1 { font-size: 2.5em; font-weight: 700; }
100
+ h2 { font-size: 1.8em; }
101
+ h3 { font-size: 1.15em; }
102
+ h4 { font-size: 0.85em; }
103
+
104
+ li { font-size: 0.9em; padding-left: 0.3em; }
105
+
106
+ blockquote {
107
+ border-left-color: var(--color-accent);
108
+ color: var(--color-dimmed);
109
+ }
110
+
111
+ th { background: var(--color-accent-bg); color: var(--color-foreground); }
112
+ th, td { border: 1px solid var(--color-border); }
113
+ tr:nth-child(even) { background: var(--color-background-stripe); }
114
+
115
+ a { color: var(--color-accent); }
116
+
117
+ .card {
118
+ background: var(--color-card-bg);
119
+ border: var(--card-border);
120
+ }
121
+ .card p { font-size: 0.9em; }
122
+
123
+ .quote { border-left-color: var(--color-accent); color: var(--color-accent); }
124
+
125
+ .kicker { color: var(--color-accent); }
126
+ .speaker { color: var(--color-foreground); }
127
+ .speaker .role { color: var(--color-dimmed); }
128
+
129
+ footer { color: var(--color-dimmed); }
130
+ .slide-num { color: var(--color-dimmed); }
131
+
132
+ .subtitle { color: var(--color-dimmed); }
133
+ .tiny { font-size: 0.7em; color: var(--color-dimmed); }
134
+ .muted { color: var(--color-dimmed); }
135
+
136
+ .note-green { border-left-color: #0fd05d; background: #e8f5e9; }
137
+ .note-yellow { border-left-color: #ffd166; background: #fff9c4; }
138
+ .note-red { border-left-color: #ff7a7a; background: #ffebee; }
139
+
140
+ /* ---- tag colors ---- */
141
+ .tag-green { border-color: var(--tag-green-border); background: var(--tag-green-bg); }
142
+ .tag-cyan { border-color: var(--tag-cyan-border); background: var(--tag-cyan-bg); }
143
+ .tag-yellow { border-color: var(--tag-yellow-border); background: var(--tag-yellow-bg); }
144
+ .tag-red { border-color: var(--tag-red-border); background: var(--tag-red-bg); }
145
+ .tag-default { border-color: var(--tag-default-border); background: var(--tag-default-bg); }
146
+ .tag-quote { border-left: 0.25em solid var(--color-accent); font-style: italic; background: none; }
@@ -0,0 +1,159 @@
1
+ /* decktile KCD Vietnam 2026 theme */
2
+
3
+ :root {
4
+ --color-background: #ffffff;
5
+ --color-background-stripe: #f0ede6;
6
+ --color-foreground: #3a2020;
7
+ --color-dimmed: #7a6a5a;
8
+ --color-accent: #7A0504;
9
+ --color-accent-bg: rgba(122, 5, 4, 0.12);
10
+ --color-border: #d6c8b8;
11
+ --color-card-bg: #F6ECD9;
12
+ --bg-image: url("assets/brand/kcd_vietnam/title_background.png");
13
+ --bg-overlay: rgba(255,255,255,0.96);
14
+ --watermark: url("assets/brand/kcd_vietnam/watermark.png");
15
+ --logo-dark: url("assets/brand/dynamia-logo-white.png");
16
+ --tag-green-bg: #e8f5e9;
17
+ --tag-cyan-bg: #fce8e8;
18
+ --tag-yellow-bg: #fef8e6;
19
+ --tag-red-bg: #fff3e0;
20
+ --tag-default-bg: #f5f5f5;
21
+ --tag-green-border: #2e7d32;
22
+ --tag-cyan-border: #7A0504;
23
+ --tag-yellow-border: #F1C560;
24
+ --tag-red-border: #F1C560;
25
+ --tag-default-border: #ccc;
26
+
27
+ --color-accent-primary: #7A0504;
28
+ --color-accent-secondary: #F1C560;
29
+ --color-accent-contrast: #1a0000;
30
+ --list-style: "\25B8 ";
31
+ --card-border: 2px solid #F1C560;
32
+ --title-h1-size: 2.5em;
33
+ --title-subtitle-size: 1.2em;
34
+ }
35
+
36
+ body {
37
+ background: #1a0000;
38
+ }
39
+
40
+ [data-theme="dark"] {
41
+ --color-background: #1a0000;
42
+ --color-background-stripe: #2d0808;
43
+ --color-foreground: #f0e8d0;
44
+ --color-dimmed: #a09080;
45
+ --color-accent: #c04040;
46
+ --color-accent-bg: rgba(192, 64, 64, 0.15);
47
+ --color-border: #3a1818;
48
+ --color-card-bg: #2d0808;
49
+ --bg-overlay: rgba(26, 0, 0, 0.90);
50
+ --tag-green-bg: #1a2e1a;
51
+ --tag-cyan-bg: #2e1a1a;
52
+ --tag-yellow-bg: #2e2a1a;
53
+ --tag-red-bg: #2e1a1a;
54
+ --tag-default-bg: #1a1a2e;
55
+ --tag-green-border: #2d8a3e;
56
+ --tag-cyan-border: #c04040;
57
+ --tag-yellow-border: #F1C560;
58
+ --tag-red-border: #F1C560;
59
+ --tag-default-border: #2a2a4a;
60
+
61
+ --color-accent-primary: #c04040;
62
+ --color-accent-secondary: #f5d68a;
63
+ --color-accent-contrast: #c0a080;
64
+ --card-border: 2px solid #F1C560;
65
+ }
66
+
67
+ section {
68
+ background: linear-gradient(var(--bg-overlay), var(--bg-overlay)), var(--bg-image) left top / cover no-repeat;
69
+ color: var(--color-foreground);
70
+ font-family: Roboto, Arial, "Helvetica Neue", sans-serif;
71
+ }
72
+
73
+ /* Title slide hides watermark */
74
+ .layout-title section::after {
75
+ background-image: none;
76
+ }
77
+ section::after {
78
+ opacity: 0.9;
79
+ }
80
+ .layout-title {
81
+ background: linear-gradient(135deg, rgba(26,0,0,0.85) 0%, rgba(122,5,4,0.85) 60%, rgba(40,2,2,0.85) 100%), var(--bg-image) left top / cover no-repeat;
82
+ color: #ffffff;
83
+ }
84
+ .layout-title h1,
85
+ .layout-title h2,
86
+ .layout-title h3,
87
+ .layout-title h4,
88
+ .layout-title h5,
89
+ .layout-title h6 { color: #ffffff; }
90
+ .layout-title .kicker { color: rgba(255,255,255,0.8); }
91
+ .layout-title .subtitle { color: #F7DE9C; }
92
+ .layout-title .speaker { color: rgba(255,255,255,0.8); }
93
+ .layout-title .speaker .role { color: rgba(255,255,255,0.5); }
94
+ .layout-title .speaker-links a { color: rgba(255,255,255,0.75); }
95
+ .layout-title .speaker-links svg { stroke: rgba(255,255,255,0.75); }
96
+
97
+ .layout-title { --logo: var(--logo-dark); }
98
+
99
+ h1, h2, h3, h4, h5, h6 {
100
+ color: var(--color-foreground);
101
+ font-family: Roboto, Arial, "Helvetica Neue", sans-serif;
102
+ }
103
+ h1 { font-size: 2.5em; font-weight: 700; }
104
+ h2 { font-size: 1.8em; padding-top: 0; }
105
+ h3 { font-size: 1.15em; }
106
+ h4 { font-size: 0.85em; }
107
+ h3::after {
108
+ display: block;
109
+ content: "";
110
+ width: 100%;
111
+ height: 0;
112
+ border-bottom: 2px solid var(--color-accent-primary);
113
+ margin-top: 0.3em;
114
+ opacity: 0.4;
115
+ }
116
+
117
+ li { font-size: 0.9em; padding-left: 0.3em; }
118
+ ul li::marker { color: var(--color-accent-primary); font-weight: bold; }
119
+
120
+ blockquote {
121
+ border-left-color: var(--color-accent-primary);
122
+ color: var(--color-dimmed);
123
+ }
124
+
125
+ th { background: var(--color-accent-bg); color: var(--color-foreground); font-weight: 700; }
126
+ th, td { border: 1px solid var(--color-border); }
127
+ tr:nth-child(even) { background: var(--color-background-stripe); }
128
+
129
+ a { color: var(--color-accent-primary); }
130
+
131
+ .card {
132
+ background: var(--color-card-bg);
133
+ border: var(--card-border);
134
+ }
135
+ .card p { font-size: 0.9em; }
136
+
137
+ .quote { border-left-color: var(--color-accent-primary); color: var(--color-accent-primary); }
138
+
139
+ .kicker { color: var(--color-accent-primary); font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; font-size: 0.75em; }
140
+ .speaker { color: var(--color-foreground); }
141
+ .speaker .role { color: var(--color-dimmed); }
142
+
143
+ footer { color: var(--color-dimmed); }
144
+ .slide-num { color: var(--color-dimmed); }
145
+
146
+ .subtitle { color: var(--color-dimmed); }
147
+ h2 + .subtitle { font-size: 0.85em; }
148
+ .tiny { font-size: 0.7em; color: var(--color-dimmed); }
149
+ .muted { color: var(--color-dimmed); }
150
+
151
+ .accent-primary { color: var(--color-accent-primary); fill: var(--color-accent-primary); stroke: var(--color-accent-primary); }
152
+ .accent-secondary { color: var(--color-accent-secondary); fill: var(--color-accent-secondary); stroke: var(--color-accent-secondary); }
153
+ .accent-contrast { color: var(--color-accent-contrast); fill: var(--color-accent-contrast); stroke: var(--color-accent-contrast); }
154
+
155
+ .tag-green { border-color: var(--tag-green-border); background: var(--tag-green-bg); }
156
+ .tag-cyan { border-color: var(--tag-cyan-border); background: var(--tag-cyan-bg); }
157
+ .tag-yellow { border-color: var(--tag-yellow-border); background: var(--tag-yellow-bg); }
158
+ .tag-red { border-color: var(--tag-red-border); background: var(--tag-red-bg); }
159
+ .tag-quote { border-left: 0.25em solid var(--color-accent-primary); font-style: italic; background: none; }