| 1 | /* |
| 2 | * Dark theme for the Zammad public Knowledge Base. |
| 3 | * |
| 4 | * Install per https://next.zammad.org/en/tutorials/custom-kb-css.html: |
| 5 | * 1. Copy this file into app/assets/stylesheets/custom_knowledge_base_public/ |
| 6 | * 2. rake assets:precompile |
| 7 | * 3. Restart Zammad |
| 8 | * 4. View the *published* KB (not the admin editor) to see the result. |
| 9 | * |
| 10 | * Built against the markup/CSS served at /help/ (Zammad's default |
| 11 | * "knowledge_base" stylesheet). No !important needed: Zammad emits |
| 12 | * app/assets/stylesheets/custom_knowledge_base_public/* as its own |
| 13 | * <link> (knowledge_base_custom-*.css), placed after both the base |
| 14 | * knowledge_base-*.css and the inline branding <style> block in |
| 15 | * <head> — verified on the live instance. Plain cascade order wins, |
| 16 | * which also means more specific rules Zammad already ships (e.g. |
| 17 | * ".article-nav-adjacent a") still take precedence over this file's |
| 18 | * broader selectors, as intended. |
| 19 | * |
| 20 | * This stylesheet does NOT overwrite your custom colors from the KB management. |
| 21 | */ |
| 22 | |
| 23 | :root { |
| 24 | --kb-bg: hsl(220, 16%, 9%); /* page background */ |
| 25 | --kb-bg-alt: hsl(220, 15%, 13%); /* header, footer, hover surfaces */ |
| 26 | --kb-surface: hsl(220, 13%, 16%); /* cards, inputs, dropdowns, code blocks */ |
| 27 | --kb-surface-hover: hsl(220, 12%, 21%); |
| 28 | --kb-border: hsl(220, 10%, 24%); |
| 29 | --kb-text: hsl(210, 20%, 92%); /* primary text / headings */ |
| 30 | --kb-text-secondary: hsl(210, 12%, 75%); |
| 31 | --kb-text-muted: hsl(210, 10%, 58%); |
| 32 | --kb-accent: #3ecb82; /* text-selection highlight only; the real link/icon accent is admin-configured */ |
| 33 | --kb-shadow: rgba(0, 0, 0, 0.55); |
| 34 | } |
| 35 | |
| 36 | /* ---------- Base ---------- */ |
| 37 | |
| 38 | html { |
| 39 | background: var(--kb-bg); |
| 40 | color: var(--kb-text-secondary); |
| 41 | } |
| 42 | |
| 43 | body { |
| 44 | background: var(--kb-bg); |
| 45 | } |
| 46 | |
| 47 | ::selection { |
| 48 | background: var(--kb-accent); |
| 49 | color: hsl(220, 16%, 9%); |
| 50 | } |
| 51 | |
| 52 | /* ---------- Header & search ---------- */ |
| 53 | |
| 54 | .header { |
| 55 | border-bottom: 1px solid var(--kb-border); |
| 56 | } |
| 57 | |
| 58 | .search-field input[type="search"] { |
| 59 | background: var(--kb-surface); |
| 60 | border: 1px solid var(--kb-border); |
| 61 | color: var(--kb-text); |
| 62 | } |
| 63 | |
| 64 | .search-field input[type="search"]::placeholder { |
| 65 | color: var(--kb-text-muted); |
| 66 | } |
| 67 | |
| 68 | .search-field .icon { |
| 69 | fill: var(--kb-text-muted); |
| 70 | } |
| 71 | |
| 72 | /* :not(:empty) matters: the base theme never boxes this <ul> — it's |
| 73 | present but invisible until JS populates it with results/a message. |
| 74 | An unconditional border/background here would show as a stray bar |
| 75 | under the search field on every page load, even with no query. */ |
| 76 | .search-results:not(:empty) { |
| 77 | background: var(--kb-surface); |
| 78 | border: 1px solid var(--kb-border); |
| 79 | } |
| 80 | |
| 81 | .search-message { |
| 82 | color: var(--kb-text-muted); |
| 83 | } |
| 84 | |
| 85 | .result a:hover { |
| 86 | border-color: var(--kb-border); |
| 87 | background: var(--kb-surface-hover); |
| 88 | } |
| 89 | |
| 90 | .result-title { |
| 91 | color: var(--kb-text); |
| 92 | } |
| 93 | |
| 94 | .result-category, |
| 95 | .result-preview { |
| 96 | color: var(--kb-text-muted); |
| 97 | } |
| 98 | |
| 99 | /* ---------- Breadcrumbs ---------- */ |
| 100 | |
| 101 | .breadcrumbs .container { |
| 102 | border-bottom: 1px solid var(--kb-border); |
| 103 | } |
| 104 | |
| 105 | .breadcrumb { |
| 106 | color: var(--kb-text-secondary); |
| 107 | } |
| 108 | |
| 109 | .breadcrumb[href]:hover { |
| 110 | color: var(--kb-text); |
| 111 | } |
| 112 | |
| 113 | .breadcrumb::after, |
| 114 | .breadcrumb::before { |
| 115 | background: var(--kb-text-muted); |
| 116 | } |
| 117 | |
| 118 | /* ---------- Category / article listing cards ---------- */ |
| 119 | |
| 120 | .main--categories h1, |
| 121 | .main--article h1, |
| 122 | h1, h2, h3, h4, h5, h6 { |
| 123 | color: var(--kb-text); |
| 124 | } |
| 125 | |
| 126 | .main--categories h1 .icon-hashtag, |
| 127 | .article-accessories .icon { |
| 128 | fill: var(--kb-text-muted); |
| 129 | } |
| 130 | |
| 131 | .feedback, |
| 132 | .button, |
| 133 | .sections--grid .section-inner, |
| 134 | .sections--list .section-inner, |
| 135 | .sections--compact-text .section-inner, |
| 136 | .box { |
| 137 | background: var(--kb-surface); |
| 138 | color: var(--kb-text-secondary); |
| 139 | box-shadow: 0 2px 10px var(--kb-shadow); |
| 140 | } |
| 141 | |
| 142 | .sections--list .section-inner:hover, |
| 143 | .sections--grid .section-inner:hover, |
| 144 | .sections--compact-text .section-inner:hover { |
| 145 | background: var(--kb-surface-hover); |
| 146 | } |
| 147 | |
| 148 | .section-category { |
| 149 | color: var(--kb-text-muted); |
| 150 | } |
| 151 | |
| 152 | .sections-empty { |
| 153 | color: var(--kb-text-muted); |
| 154 | border: 1px dashed var(--kb-border); |
| 155 | } |
| 156 | |
| 157 | .sections + hr { |
| 158 | border-top: 1px solid var(--kb-border); |
| 159 | } |
| 160 | |
| 161 | .not-published-note { |
| 162 | color: var(--kb-text-muted); |
| 163 | } |
| 164 | |
| 165 | /* ---------- Article content ---------- */ |
| 166 | |
| 167 | .article-content { |
| 168 | color: var(--kb-text-secondary); |
| 169 | } |
| 170 | |
| 171 | .article-meta { |
| 172 | color: var(--kb-text-muted); |
| 173 | } |
| 174 | |
| 175 | .article-content table { |
| 176 | box-shadow: 0 0 0 1px var(--kb-border), 0 2px var(--kb-border); |
| 177 | } |
| 178 | |
| 179 | .article-content td, |
| 180 | .article-content th { |
| 181 | border: 1px solid var(--kb-border); |
| 182 | } |
| 183 | |
| 184 | .article-content th { |
| 185 | background: var(--kb-surface); |
| 186 | color: var(--kb-text); |
| 187 | } |
| 188 | |
| 189 | .article-content blockquote { |
| 190 | border-left: 5px solid var(--kb-border); |
| 191 | color: var(--kb-text-secondary); |
| 192 | } |
| 193 | |
| 194 | .article-content code, |
| 195 | .article-content pre { |
| 196 | background: var(--kb-surface); |
| 197 | color: var(--kb-text); |
| 198 | } |
| 199 | |
| 200 | .article-content hr { |
| 201 | border-top: 1px solid var(--kb-border); |
| 202 | } |
| 203 | |
| 204 | /* Zammad's own .article-nav-adjacent a is already a subtle gray — left alone. |
| 205 | Its :hover darkens toward near-black for a light page, which would go |
| 206 | near-invisible on a dark one, so that part alone needs a dark-mode value. */ |
| 207 | .article-nav-adjacent a:hover { |
| 208 | color: var(--kb-text); |
| 209 | fill: var(--kb-text); |
| 210 | } |
| 211 | |
| 212 | /* Photos/screenshots keep their real colors; just soften pure-white glare a touch */ |
| 213 | .article-content img { |
| 214 | filter: brightness(0.92); |
| 215 | } |
| 216 | |
| 217 | /* ---------- Attachments & tags ---------- */ |
| 218 | |
| 219 | .attachment { |
| 220 | border-bottom: 1px solid var(--kb-border); |
| 221 | color: var(--kb-text-secondary); |
| 222 | } |
| 223 | |
| 224 | .attachment:hover { |
| 225 | background: var(--kb-surface-hover); |
| 226 | } |
| 227 | |
| 228 | .attachment-size { |
| 229 | color: var(--kb-text-muted); |
| 230 | } |
| 231 | |
| 232 | /* ---------- Image lightbox preview ---------- */ |
| 233 | |
| 234 | .kb-image-preview { |
| 235 | background: hsla(220, 16%, 9%, 0.94); |
| 236 | } |
| 237 | |
| 238 | .kb-image-preview-image { |
| 239 | background: var(--kb-surface); |
| 240 | border: 1px solid var(--kb-border); |
| 241 | box-shadow: 0 2px 10px var(--kb-shadow); |
| 242 | } |
| 243 | |
| 244 | /* ---------- Footer & language picker ---------- */ |
| 245 | |
| 246 | .footer { |
| 247 | border-top: 1px solid var(--kb-border); |
| 248 | background: var(--kb-bg-alt); |
| 249 | color: var(--kb-text-muted); |
| 250 | } |
| 251 | |
| 252 | .footer .menu-item { |
| 253 | color: var(--kb-text-secondary); |
| 254 | } |
| 255 | |
| 256 | /* ---------- Buttons & dropdowns ---------- */ |
| 257 | |
| 258 | .btn { |
| 259 | border-color: var(--kb-border); |
| 260 | color: var(--kb-text); |
| 261 | } |
| 262 | |
| 263 | .dropdown-menu { |
| 264 | background: var(--kb-surface); |
| 265 | box-shadow: 0 15px 35px var(--kb-shadow), 0 5px 15px var(--kb-shadow); |
| 266 | } |
| 267 | |
| 268 | .dropdown-menu::before { |
| 269 | background: var(--kb-surface); |
| 270 | } |
| 271 | |
| 272 | .dropdown-menu a { |
| 273 | color: var(--kb-text); |
| 274 | } |
| 275 | |
| 276 | /* Checkmark on the selected entry: dim it relative to the bright label text */ |
| 277 | .dropdown-menu .icon { |
| 278 | fill: var(--kb-text-secondary); |
| 279 | } |
| 280 | |
| 281 | .dropdown-menu a:hover, |
| 282 | .dropdown-menu a:focus { |
| 283 | background: var(--kb-surface-hover); |
| 284 | } |
| 285 | |
| 286 | .dropdown-menu li.is-disabled a { |
| 287 | color: var(--kb-text-muted); |
| 288 | } |
| 289 | |
| 290 | /* ---------- Scrollbar (WebKit/Blink) ---------- */ |
| 291 | |
| 292 | ::-webkit-scrollbar { |
| 293 | width: 12px; |
| 294 | height: 12px; |
| 295 | } |
| 296 | |
| 297 | ::-webkit-scrollbar-track { |
| 298 | background: var(--kb-bg); |
| 299 | } |
| 300 | |
| 301 | ::-webkit-scrollbar-thumb { |
| 302 | background: var(--kb-border); |
| 303 | border-radius: 6px; |
| 304 | } |
| 305 | |
| 306 | ::-webkit-scrollbar-thumb:hover { |
| 307 | background: var(--kb-surface-hover); |
| 308 | } |
| 309 |
MrGeneration / Dark Zammad Knowledge Base Stylesheet
Last active 14 hours ago