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.
- decktile/__init__.py +5 -0
- decktile/__main__.py +3 -0
- decktile/assets/lucide/VENDORED.md +21 -0
- decktile/assets/lucide/lucide.css +2193 -0
- decktile/assets/lucide/lucide.ttf +3 -0
- decktile/cli.py +269 -0
- decktile/parser/__init__.py +0 -0
- decktile/parser/ast.py +191 -0
- decktile/parser/markdown.py +289 -0
- decktile/plugins/cards.py +25 -0
- decktile/plugins/directives.py +56 -0
- decktile/plugins/fenced.py +228 -0
- decktile/plugins/layouts.py +69 -0
- decktile/plugins/lucide.py +65 -0
- decktile/plugins/lucide_font.py +26 -0
- decktile/render/__init__.py +0 -0
- decktile/render/html.py +287 -0
- decktile/render/ir.py +536 -0
- decktile/render/odf/__init__.py +4 -0
- decktile/render/odf/geometry.py +510 -0
- decktile/render/odf/shapes.py +719 -0
- decktile/render/odf/styles.py +828 -0
- decktile/render/odp.py +380 -0
- decktile/render/pdf.py +17 -0
- decktile/render/pdg.py +72 -0
- decktile/render/seaborn.py +155 -0
- decktile/render/templates/base.css +741 -0
- decktile/render/templates/decktile.js +371 -0
- decktile/render/templates/shell.html +106 -0
- decktile/seaborn_styles/__init__.py +5 -0
- decktile/seaborn_styles/ossummit_europe.py +30 -0
- decktile/theme/__init__.py +0 -0
- decktile/theme/colors.py +53 -0
- decktile/theme/loader.py +22 -0
- decktile/theme/parser.py +264 -0
- decktile/themes/default.css +146 -0
- decktile/themes/kcd_vietnam.css +159 -0
- decktile/themes/kubecon_china.css +213 -0
- decktile/themes/kubecon_japan.css +156 -0
- decktile/themes/ossummit_europe.css +196 -0
- decktile/themes/ossummit_korea.css +196 -0
- decktile-0.2.0.dist-info/METADATA +625 -0
- decktile-0.2.0.dist-info/RECORD +46 -0
- decktile-0.2.0.dist-info/WHEEL +4 -0
- decktile-0.2.0.dist-info/entry_points.txt +6 -0
- decktile-0.2.0.dist-info/licenses/LICENSE +674 -0
decktile/theme/parser.py
ADDED
|
@@ -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; }
|