(defcomp ~shared:pages/base-shell (&key (title :as string) (asset-url :as string) &rest children)
  (<>
    (raw! "<!doctype html>")
    (html :lang "en"
      (head
        (meta :charset "utf-8")
        (meta :name "viewport" :content "width=device-width, initial-scale=1")
        (title title)
        (style
          "body{margin:0;min-height:100vh;display:flex;align-items:center;"
          "justify-content:center;font-family:system-ui,sans-serif;"
          "background:#fafaf9;color:#1c1917}")
        (link :rel "stylesheet" :type "text/css" :href (str asset-url "/styles/tw.css"))
        (link :rel "stylesheet" :href (str asset-url "/fontawesome/css/all.min.css")))
      (body :class "bg-stone-50 text-stone-900"
        children))))

;; ---------------------------------------------------------------------------
;; Suspense — streaming placeholder that renders fallback until resolved.
;;
;; Server-side: rendered in the initial streaming chunk with a fallback.
;; Client-side: replaced when the server streams a resolution chunk via
;;   <script>__sxResolve("id", "(resolved sx ...)")</script>
;; ---------------------------------------------------------------------------

(defcomp ~shared:pages/suspense (&key (id :as string) fallback &rest children)
  (div :id (str "sx-suspense-" id)
       :data-suspense id
       :style "display:contents"
    (if (not (empty? children)) children fallback)))

(defcomp ~shared:pages/error-page (&key (title :as string) (message :as string) (image :as string?) (asset-url :as string))
  (~shared:pages/base-shell :title title :asset-url asset-url
    (div :class "text-center p-8 max-w-lg mx-auto"
      (div :class "font-bold text-2xl md:text-4xl text-red-500 mb-4"
        (div message))
      (when image
        (div :class "flex justify-center"
          (img :src image :width "300" :height "300"))))))
