From 301636d59791b8aa3648bb12ced2f2d35adba420 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 30 Aug 2022 21:33:28 +1200 Subject: [PATCH] Add homepage, architecture changes * Create homepage * Page data is automatically reloaded (except when compiling) * Entrypoint is breezewiki.rkt for running and dist.rkt for compiling * Include stack trace when sending error messages --- .gitignore | 3 + breezewiki.rkt | 30 ++++-- dist.rkt | 43 ++++++++ src/page-home.rkt | 77 +++++++++++++++ src/page-search.rkt | 2 +- src/reloadable.rkt | 150 ++++++++++++++++++++++++++++ src/server-utils.rkt | 16 +++ src/xexpr-utils.rkt | 5 +- static/internal-background.png | Bin 0 -> 1500 bytes static/internal.css | 173 +++++++++++++++++++++++++++++++++ 10 files changed, 486 insertions(+), 13 deletions(-) create mode 100644 dist.rkt create mode 100644 src/page-home.rkt create mode 100644 src/reloadable.rkt create mode 100644 src/server-utils.rkt create mode 100644 static/internal-background.png create mode 100644 static/internal.css diff --git a/.gitignore b/.gitignore index caf5d54..17b94c5 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ # Compiled compiled +/breezewiki +/dist +/breezewiki-dist # Personal /config.ini diff --git a/breezewiki.rkt b/breezewiki.rkt index 1f39521..22f368c 100644 --- a/breezewiki.rkt +++ b/breezewiki.rkt @@ -10,23 +10,33 @@ (prefix-in filter: web-server/dispatchers/dispatch-filter) (prefix-in files: web-server/dispatchers/dispatch-files) "src/config.rkt" - "src/page-category.rkt" - "src/page-not-found.rkt" - "src/page-proxy.rkt" - "src/page-wiki.rkt" - "src/page-search.rkt") + "src/reloadable.rkt" + "src/server-utils.rkt") + +(define-syntax-rule (require-reloadable filename varname) + (define varname + (reloadable-entry-point->procedure + (make-reloadable-entry-point (quote varname) filename)))) + +(require-reloadable "src/page-category.rkt" page-category) +(require-reloadable "src/page-home.rkt" page-home) +(require-reloadable "src/page-not-found.rkt" page-not-found) +(require-reloadable "src/page-proxy.rkt" page-proxy) +(require-reloadable "src/page-search.rkt" page-search) +(require-reloadable "src/page-wiki.rkt" page-wiki) + +(when (not (config-true? 'debug)) + (set-reload-poll-interval! #f)) +(reload!) (define-runtime-path path-static "static") -(define mime-types - (hash #".css" #"text/css" - #".svg" #"image/svg+xml")) - (serve/launch/wait #:listen-ip (if (config-true? 'debug) "127.0.0.1" #f) #:port (string->number (config-get 'port)) (λ (quit) (sequencer:make + (pathprocedure:make "/" page-home) (pathprocedure:make "/proxy" page-proxy) (filter:make #rx"^/[a-z-]+/wiki/Category:.+$" (lift:make page-category)) (filter:make #rx"^/[a-z-]+/wiki/.+$" (lift:make page-wiki)) @@ -38,6 +48,6 @@ (struct-copy url u [path (cdr (url-path u))]))) #:path->mime-type (lambda (u) - (hash-ref mime-types (path-get-extension u))) + (ext->mime-type (path-get-extension u))) #:cache-no-cache (config-true? 'debug) #;"browser applies heuristics if unset")) (lift:make page-not-found)))) diff --git a/dist.rkt b/dist.rkt new file mode 100644 index 0000000..19fc736 --- /dev/null +++ b/dist.rkt @@ -0,0 +1,43 @@ +#lang racket/base +(require racket/path + racket/runtime-path + net/url + web-server/servlet-dispatch + web-server/dispatchers/filesystem-map + (prefix-in pathprocedure: web-server/dispatchers/dispatch-pathprocedure) + (prefix-in sequencer: web-server/dispatchers/dispatch-sequencer) + (prefix-in lift: web-server/dispatchers/dispatch-lift) + (prefix-in filter: web-server/dispatchers/dispatch-filter) + (prefix-in files: web-server/dispatchers/dispatch-files) + "src/config.rkt" + "src/server-utils.rkt") + +(require (only-in "src/page-category.rkt" page-category)) +(require (only-in "src/page-home.rkt" page-home)) +(require (only-in "src/page-not-found.rkt" page-not-found)) +(require (only-in "src/page-proxy.rkt" page-proxy)) +(require (only-in "src/page-search.rkt" page-search)) +(require (only-in "src/page-wiki.rkt" page-wiki)) + +(define-runtime-path path-static "static") + +(serve/launch/wait + #:listen-ip (if (config-true? 'debug) "127.0.0.1" #f) + #:port (string->number (config-get 'port)) + (λ (quit) + (sequencer:make + (pathprocedure:make "/" page-home) + (pathprocedure:make "/proxy" page-proxy) + (filter:make #rx"^/[a-z-]+/wiki/Category:.+$" (lift:make page-category)) + (filter:make #rx"^/[a-z-]+/wiki/.+$" (lift:make page-wiki)) + (filter:make #rx"^/[a-z-]+/search$" (lift:make page-search)) + (filter:make #rx"^/static/" (files:make + #:url->path + (lambda (u) + ((make-url->path path-static) + (struct-copy url u [path (cdr (url-path u))]))) + #:path->mime-type + (lambda (u) + (ext->mime-type (path-get-extension u))) + #:cache-no-cache (config-true? 'debug) #;"browser applies heuristics if unset")) + (lift:make page-not-found)))) diff --git a/src/page-home.rkt b/src/page-home.rkt new file mode 100644 index 0000000..c42102e --- /dev/null +++ b/src/page-home.rkt @@ -0,0 +1,77 @@ +#lang racket/base + +(require html-writing + web-server/http + "xexpr-utils.rkt" + "config.rkt") + +(provide + page-home) + +(module+ test + (require rackunit)) + +(define examples + '(("crosscode" "CrossCode_Wiki") + ("minecraft" "Bricks") + ("undertale" "Hot_Dog...%3F") + ("tardis" "Eleanor_Blake") + ("fireemblem" "God-Shattering_Star") + ("fallout" "Pip-Boy_3000"))) + +(define content + `((h2 "BreezeWiki makes wiki pages on Fandom readable") + (p "It removes ads, videos, and suggested content, leaving you with a clean page that doesn't consume all your data.") + (p "If you're looking for an \"alternative\" to Fandom for writing pages, you should look elsewhere. BreezeWiki only lets you read existing pages.") + (p "BreezeWiki can also be called an \"alternative frontend for Fandom\".") + (h2 "Example pages") + (ul + ,@(map (λ (x) + `(li (a (@ (href ,(apply format "/~a/wiki/~a" x))) + ,(apply format "~a: ~a" x)))) + examples)) + (h2 "How to use") + (p "While browsing any page on Fandom, you can replace \"fandom.com\" in the address bar with \"breezewiki.com\" to see the BreezeWiki version of that page.") + (p "After that, you can click the links to navigate around the pages.") + (p "To get back to Fandom, click the link that's at the bottom of the page."))) + +(define body + `(html + (head + (meta (@ (name ")viewport") (content "width=device-width, initial-scale=1"))) + (title "About | BreezeWiki") + (link (@ (rel "stylesheet") (type "text/css") (href "/static/internal.css"))) + (link (@ (rel "stylesheet") (type "text/css") (href "/static/main.css")))) + (body (@ (class "skin-fandomdesktop theme-fandomdesktop-light internal")) + (div (@ (class "main-container")) + (div (@ (class "fandom-community-header__background tileBoth header"))) + (div (@ (class "page")) + (main (@ (class "page__main")) + (div (@ (class "custom-top")) + (h1 (@ (class "page-title")) + "About BreezeWiki")) + (div (@ (id "content") #;(class "page-content")) + (div (@ (id "mw-content-text")) + ,@content)) + (footer (@ (class "custom-footer")) + (div (@ (class "internal-footer")) + (img (@ (class "my-logo") (src "/static/breezewiki.svg"))) + ,(if (config-get 'instance-is-official) + `(p ,(format "This instance is run by the ~a developer, " (config-get 'application-name)) + (a (@ (href "https://cadence.moe/contact")) + "Cadence.")) + `(p + ,(format "This unofficial instance is based off the ~a source code, but is not controlled by the code developer." (config-get 'application-name)))) + (p "Text content on wikis run by Fandom is available under the Creative Commons Attribution-Share Alike License 3.0 (Unported), " + (a (@ (href "https://www.fandom.com/licensing")) "see license info.") + " Media files and official Fandom documents have different copying restrictions.") + (p ,(format "Fandom is a trademark of Fandom, Inc. ~a is not affiliated with Fandom." (config-get 'application-name))))))))))) +(module+ test + (check-not-false (xexp->html body))) + +(define (page-home req) + (response/output + #:code 200 + (λ (out) + (write-html body out)))) + diff --git a/src/page-search.rkt b/src/page-search.rkt index 5948af0..7b0abe3 100644 --- a/src/page-search.rkt +++ b/src/page-search.rkt @@ -69,7 +69,7 @@ (define data (easy:response-json dest-res)) (define body (generate-results-page dest-url wikiname query data)) - (when (config-get 'debug) + (when (config-true? 'debug) ; used for its side effects ; convert to string with error checking, error will be raised if xexp is invalid (xexp->html body)) diff --git a/src/reloadable.rkt b/src/reloadable.rkt new file mode 100644 index 0000000..61c93c5 --- /dev/null +++ b/src/reloadable.rkt @@ -0,0 +1,150 @@ +#lang racket/base + +;;; Source: https://github.com/tonyg/racket-reloadable/blob/master/reloadable/main.rkt +;;; Source commit: cae2a14 from 24 May 2015 +;;; Source license: LGPL 3 or later + +(provide (struct-out reloadable-entry-point) + reload-poll-interval + set-reload-poll-interval! + reload-failure-retry-delay + reload! + make-reloadable-entry-point + lookup-reloadable-entry-point + reloadable-entry-point->procedure + make-persistent-state) + +(require racket/set) +(require racket/string) +(require racket/match) +(require racket/rerequire) + +(define reload-poll-interval 0.5) ;; seconds +(define reload-failure-retry-delay (make-parameter 5)) ;; seconds + +(struct reloadable-entry-point (name + module-path + identifier-symbol + on-absent + [value #:mutable]) + #:prefab) + +(define reloadable-entry-points (make-hash)) +(define persistent-state (make-hash)) + +(define (set-reload-poll-interval! v) + (set! reload-poll-interval v)) + +(define (reloader-main) + (let loop () + (match (sync (handle-evt (thread-receive-evt) + (lambda (_) (thread-receive))) + (if reload-poll-interval + (handle-evt (alarm-evt (+ (current-inexact-milliseconds) + (* reload-poll-interval 1000))) + (lambda (_) (list #f 'reload))) + never-evt)) + [(list ch 'reload) + (define result (do-reload!)) + (when (not result) (sleep (reload-failure-retry-delay))) + (when ch (channel-put ch result))]) + (loop))) + +(define reloader-thread (thread reloader-main)) + +(define (reloader-rpc . request) + (define ch (make-channel)) + (thread-send reloader-thread (cons ch request)) + (channel-get ch)) + +(define (reload!) (reloader-rpc 'reload)) + +(define first-load? #t) +(define (say-loading-once! port) + (when first-load? + (display "loading support files" port) + (set! first-load? #f))) + +(define (handle-loader-output) + (define i (thread-receive)) + (define real-error-port (thread-receive)) + (say-loading-once! real-error-port) + (let loop () + (let ([line (read-line i)]) + (cond + [(eof-object? line) + (void)] + [(string-contains? line "[load") + (display "." real-error-port) + (loop)] + [#t + (displayln line real-error-port) + (loop)])))) + +;; Only to be called from reloader-main +(define (do-reload!) + (define module-paths (for/set ((e (in-hash-values reloadable-entry-points))) + (reloadable-entry-point-module-path e))) + (with-handlers ((exn:fail? + (lambda (e) + (log-error "*** WHILE RELOADING CODE***\n~a" + (parameterize ([current-error-port (open-output-string)]) + ((error-display-handler) (exn-message e) e) + (get-output-string (current-error-port)))) + #f))) + (for ((module-path (in-set module-paths))) + (let ([real-error-port (current-error-port)]) + (define-values (i o) (make-pipe)) + (parameterize ([current-error-port o]) + (define new-thread (thread handle-loader-output)) + (thread-send new-thread i) + (thread-send new-thread real-error-port) + (dynamic-rerequire module-path #:verbosity 'all)))) + (for ((e (in-hash-values reloadable-entry-points))) + (match-define (reloadable-entry-point _ module-path identifier-symbol on-absent _) e) + (define new-value (if on-absent + (dynamic-require module-path identifier-symbol on-absent) + (dynamic-require module-path identifier-symbol))) + (set-reloadable-entry-point-value! e new-value)) + #t)) + +(define (make-reloadable-entry-point name module-path [identifier-symbol name] + #:on-absent [on-absent #f]) + (define key (list module-path name)) + (hash-ref reloadable-entry-points + key + (lambda () + (define e (reloadable-entry-point name module-path identifier-symbol on-absent #f)) + (hash-set! reloadable-entry-points key e) + e))) + +(define (lookup-reloadable-entry-point name module-path) + (hash-ref reloadable-entry-points + (list module-path name) + (lambda () + (error 'lookup-reloadable-entry-point + "Reloadable-entry-point ~a not found in module ~a" + name + module-path)))) + +(define (reloadable-entry-point->procedure e) + (make-keyword-procedure + (lambda (keywords keyword-values . positionals) + (keyword-apply (reloadable-entry-point-value e) + keywords + keyword-values + positionals)))) + +(define (make-persistent-state name initial-value-thunk) + (hash-ref persistent-state + name + (lambda () + (define value (initial-value-thunk)) + (define handler + (case-lambda + [() value] + [(new-value) + (set! value new-value) + value])) + (hash-set! persistent-state name handler) + handler))) diff --git a/src/server-utils.rkt b/src/server-utils.rkt new file mode 100644 index 0000000..f41c2e6 --- /dev/null +++ b/src/server-utils.rkt @@ -0,0 +1,16 @@ +#lang racket/base + +(provide + ext->mime-type) + +(module+ test + (require rackunit)) + +(define hash-ext-mime-type + (hash #".css" #"text/css" + #".svg" #"image/svg+xml" + #".png" #"image/png")) +(define (ext->mime-type ext) + (hash-ref hash-ext-mime-type ext)) +(module+ test + (check-equal? (ext->mime-type #".png") #"image/png")) diff --git a/src/xexpr-utils.rkt b/src/xexpr-utils.rkt index a6bf1ca..b2b4422 100644 --- a/src/xexpr-utils.rkt +++ b/src/xexpr-utils.rkt @@ -195,6 +195,7 @@ #:mime-type #"text/plain" (λ (out) (for ([port (list (current-output-port) out)]) - (displayln "Exception raised in Racket code at response generation time:" port) - (displayln (exn-message e) port)))))]) + (parameterize ([current-error-port out]) + (displayln "Exception raised in Racket code at response generation time:" (current-error-port)) + ((error-display-handler) (exn-message e) e))))))]) body ...)) diff --git a/static/internal-background.png b/static/internal-background.png new file mode 100644 index 0000000000000000000000000000000000000000..db9e06641337aad788e968bd0724afbcd6b6d096 GIT binary patch literal 1500 zcmV<21ta>2P) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O1b@mV_n{{NJa@5$JB3kK-W7d$~c5p8}m(cGo1k z8hKg>*fdpG&d~nXpPl}|A&!N-#T0S~4u{V^OF<>MYXS;skGMpTta@oOlP5z{KnONT$$>)w@uTbAf?!Ben_hWU%2QV7W z9p&i7&Ugj`H&Cwgv49eB8tQSDjFMb&72-5k-G)**vp|(j?74B_&ilm56)AezB?3jN z=xj?GB};{3tbP>k5;bdsf}&M+f_z@(oeR%-^PI18!pw~^R2ac{%Ks0A{;l&jg?g`p zB6jJ`74nj?bCJeSZd@@6f^a`|vq|u)obtP6|6r>^L7OxuEU>uk2(cM|>@A%-E5;e{ zcq^Dsy%#CLv&#__> zWHiH@AcSeq2yLRu|3sg#hl;6rGYb|ik!34EgAW!$h+&vqi5h*h7-Ebm=2(&@ zpDcwGQ%X6NELq`!mP3v?<(x|qDwwDsR>Am!mQ6O@{AOF&;+D3&m5S|CeYF~Dtf}T& znlx>&i56OHspVF7rX5bP``z}i$35+NFCBZWYxmuH=&`4sdwHvNs`{<`LNz{Bld04g z7vHL(m12B_;9(~=G=pN|8H&fD05-G?&0J#dWhgf^b9s2036iv-4K*H$K|;T?Sm(EP zAIkkmH?s62-Q>TNqaoe@p&UWF@48*6*2Z_PPQ{M5aB%7rZ{H4+!9Gf*r=%^qGHzjm zzeLH#-q>ivmt$5T<1djx=m>{3*O3vb<3sy9EH+|Us3Fa$M&t81DnCbSb5$FwF6*RQ zgk11Jw+OjdFWn;KV%>C$kPD<+gj^uqBIE+;79kf%w+OjFx<$wZ(k((R_@G;aTp-4^ueM9~uuTGvZYJZMFF;z29Y6AxOEo_NrT^u&W!q$eJKiXyt>Rc+koR-S;5;Lcu>}`WwYyj+MV?^E?0m0fcEoLr_UWLm+T+Z)Rz1WdHzp zoPCi!NW(xJ#a~m!q7?@_6miH0dehQ5?-PeuNm7W< ziN_4OAn_yDWtZPL7abOOX2{5-=ZQnaVzGne4rV1oC5{nC6jh^qA?vcjd5g1Jsj=2Q z`3r-2ZDpD3G>4JEB9@SX2pKh$QGtahtr{sN(zGA<@DDlu6uD$_mBGldfGSi-jvxFF zes^mYrY7B_U>xXvvF(o$pl26o)@}Ry*tVM|fd3h|(%SxN1DN?Fz24R$N5H@~aB-w)B}a?QRyZ;b!|00v@9M??VM0N()LVqfmR00009a7bBm000XU000XU0RWnu z7ytkO2XskIMF-{x7z;T$^uUbW0000xNkl=z literal 0 HcmV?d00001 diff --git a/static/internal.css b/static/internal.css new file mode 100644 index 0000000..6cc9294 --- /dev/null +++ b/static/internal.css @@ -0,0 +1,173 @@ +:root { + --theme-body-dynamic-color-1: #fff; + --theme-body-dynamic-color-1--rgb: 255,255,255; + --theme-body-dynamic-color-2: #e6e6e6; + --theme-body-dynamic-color-2--rgb: 230,230,230; + --theme-page-dynamic-color-1: #000; + --theme-page-dynamic-color-1--rgb: 0,0,0; + --theme-page-dynamic-color-1--inverted: #fff; + --theme-page-dynamic-color-1--inverted--rgb: 255,255,255; + --theme-page-dynamic-color-2: #3a3a3a; + --theme-page-dynamic-color-2--rgb: 58,58,58; + --theme-sticky-nav-dynamic-color-1: #000; + --theme-sticky-nav-dynamic-color-1--rgb: 0,0,0; + --theme-sticky-nav-dynamic-color-2: #3a3a3a; + --theme-sticky-nav-dynamic-color-2--rgb: 58,58,58; + --theme-link-dynamic-color-1: #000; + --theme-link-dynamic-color-1--rgb: 0,0,0; + --theme-link-dynamic-color-2: #3a3a3a; + --theme-link-dynamic-color-2--rgb: 58,58,58; + --theme-accent-dynamic-color-1: #fff; + --theme-accent-dynamic-color-1--rgb: 255,255,255; + --theme-accent-dynamic-color-2: #e6e6e6; + --theme-accent-dynamic-color-2--rgb: 230,230,230; + --theme-body-background-color: #286cab; + --theme-body-background-color--rgb: 40,108,171; + + --theme-body-text-color: #fff; + --theme-body-text-color--rgb: 255,255,255; + --theme-body-text-color--hover: #cccccc; + --theme-sticky-nav-background-color: #ffffff; + --theme-sticky-nav-background-color--rgb: 255,255,255; + --theme-sticky-nav-text-color: #000; + --theme-sticky-nav-text-color--hover: #333333; + --theme-page-background-color: #ffffff; + --theme-page-background-color--rgb: 255,255,255; + --theme-page-background-color--secondary: #f2f2f2; + --theme-page-background-color--secondary--rgb: 242,242,242; + --theme-page-text-color: #3a3a3a; + --theme-page-text-color--rgb: 58,58,58; + --theme-page-text-color--hover: #6d6d6d; + --theme-page-text-mix-color: #9d9d9d; + --theme-page-text-mix-color-95: #f5f5f5; + --theme-page-accent-mix-color: #b6b6b6; + --theme-page-headings-font: 'Rubik'; + --theme-link-color: #8a8a8a; + --theme-link-color--rgb: 138,138,138; + --theme-link-color--hover: #565656; + --theme-link-label-color: #000; + --theme-accent-color: #6c6c6c; + --theme-accent-color--rgb: 108,108,108; + --theme-accent-color--hover: #9f9f9f; + --theme-accent-label-color: #fff; + --theme-border-color: #cecece; + --theme-border-color--rgb: 206,206,206; + --theme-alert-color: #bf0017; + --theme-alert-color--rgb: 191,0,23; + --theme-alert-color--hover: #59000a; + --theme-alert-color--secondary: #bf0017; + --theme-alert-label: #fff; + --theme-warning-color: #cf721c; + --theme-warning-color--rgb: 207,114,28; + --theme-warning-color--secondary: #ce711b; + --theme-warning-label: #000; + --theme-success-color: #0c742f; + --theme-success-color--rgb: 12,116,47; + --theme-success-color--secondary: #0c742f; + --theme-success-label: #fff; + --theme-message-color: #753369; + --theme-message-label: #fff; + --theme-community-header-color: #000000; + --theme-community-header-color--hover: #333333; + --theme-background-image-opacity: 100%; + --theme-page-text-opacity-factor: 0.85; + --theme-body-text-opacity-factor: 0.7; +} + +.skin-fandomdesktop .CodeMirror{ + --codemirror-yellow: #a88d00; + --codemirror-light-blue: #0096fb; + --codemirror-blue: #08f; + --codemirror-green: #290; + --codemirror-red: #f50; + --codemirror-dark-red: #a11; + --codemirror-purple: #80c; + --codemirror-pink: #e0e; + --codemirror-light-gray: #929292; + --codemirror-gray: #789797; +} + +.mw-highlight { + --pygments-background: #f4f3f4; + --pygments-err: #f00; + --pygments-c: #408080; + --pygments-k: #008000; + --pygments-o: #666; + --pygments-ch: #408080; + --pygments-cm: #408080; + --pygments-cp: #b17300; + --pygments-cpf: #408080; + --pygments-c1: #408080; + --pygments-cs: #408080; + --pygments-gd: #a00000; + --pygments-gr: #f00; + --pygments-gh: #000080; + --pygments-gi: #009500; + --pygments-go: #808080; + --pygments-gp: #000080; + --pygments-gu: #800080; + --pygments-gt: #04d; + --pygments-kc: #008000; + --pygments-kd: #008000; + --pygments-kn: #008000; + --pygments-kp: #008000; + --pygments-kr: #008000; + --pygments-kt: #b00040; + --pygments-m: #666; + --pygments-s: #ba2121; + --pygments-na: #768826; + --pygments-nb: #008000; + --pygments-nc: #00f; + --pygments-no: #800; + --pygments-nd: #a2f; + --pygments-ni: #7f7f7f; + --pygments-ne: #d2413a; + --pygments-nf: #00f; + --pygments-nl: #818100; + --pygments-nn: #00f; + --pygments-nt: #008000; + --pygments-nv: #19177c; + --pygments-ow: #a2f; + --pygments-w: #808080; + --pygments-mb: #666; + --pygments-mf: #666; + --pygments-mh: #666; + --pygments-mi: #666; + --pygments-mo: #666; + --pygments-sa: #ba2121; + --pygments-sb: #ba2121; + --pygments-sc: #ba2121; + --pygments-dl: #ba2121; + --pygments-sd: #ba2121; + --pygments-s2: #ba2121; + --pygments-se: #b62; + --pygments-sh: #ba2121; + --pygments-si: #b68; + --pygments-sx: #008000; + --pygments-sr: #b68; + --pygments-s1: #ba2121; + --pygments-ss: #19177c; + --pygments-bp: #008000; + --pygments-fm: #00f; + --pygments-vc: #19177c; + --pygments-vg: #19177c; + --pygments-vi: #19177c; + --pygments-vm: #19177c; + --pygments-il: #666; +} + +body { + background: linear-gradient(to bottom, rgba(225, 133, 155, 0) 15%, rgba(225, 133, 155, 0.8)), url(/static/internal-background.png); + min-height: 100vh; + padding: 4px; + margin: 0; +} + +.page__main { + border-radius: 3px; + padding: 24px 36px; +} + +.internal-footer { + max-width: 700px; +}