+ ft_html and ft_hx functions to add some conveniences to ft, along with a full set of basic HTML components, and functions to work with forms and FT conversion
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
from lxml import html as lx
+from pprint import pprint
Renders FT Components into HTML within a Jupyter notebook.
+
+
sentence = P(Strong("FastHTML is ", I("Fast")))
+
+# When placed within the `show()` function, this will render
+# the HTML in Jupyter notebooks.
+show(sentence)
+
+
+FastHTML is Fast
+
+
+
+
# Called without the `show()` function, the raw HTML is displayed
+sentence
+
+
+
+
+
\ No newline at end of file
diff --git a/api/components.html.md b/api/components.html.md
new file mode 100644
index 00000000..453f46d1
--- /dev/null
+++ b/api/components.html.md
@@ -0,0 +1,319 @@
+# Components
+
+
+
+
+``` python
+from lxml import html as lx
+from pprint import pprint
+```
+
+------------------------------------------------------------------------
+
+source
+
+### show
+
+> show (ft, *rest)
+
+*Renders FT Components into HTML within a Jupyter notebook.*
+
+``` python
+sentence = P(Strong("FastHTML is ", I("Fast")))
+
+# When placed within the `show()` function, this will render
+# the HTML in Jupyter notebooks.
+show(sentence)
+```
+
+
+FastHTML is Fast
+
+``` python
+# Called without the `show()` function, the raw HTML is displayed
+sentence
+```
+
+``` html
+
+ The FastHTML subclass of Starlette, along with the RouterX and RouteX classes it automatically uses.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
This is the source code to fasthtml. You won’t need to read this unless you want to understand how things are built behind the scenes, or need full details of a particular API. The notebook is converted to the Python module fasthtml/core.py using nbdev.
We write source code first, and then tests come after. The tests serve as both a means to confirm that the code works and also serves as working examples. The first declared function, date, is an example of this pattern.
debug - Boolean indicating if debug tracebacks should be returned on errors.
+
routes - A list of routes to serve incoming HTTP and WebSocket requests.
+
middleware - A list of middleware to run for every request. A starlette application will always automatically include two middleware classes. ServerErrorMiddleware is added as the very outermost middleware, to handle any uncaught errors occurring anywhere in the entire stack. ExceptionMiddleware is added as the very innermost middleware, to deal with handled exception cases occurring in the routing or endpoints.
+
exception_handlers - A mapping of either integer status codes, or exception class types onto callables which handle the exceptions. Exception handler callables should be of the form handler(request, exc) -> response and may be either standard functions, or async functions.
+
on_startup - A list of callables to run on application startup. Startup handler callables do not take any arguments, and may be either standard functions, or async functions.
+
on_shutdown - A list of callables to run on application shutdown. Shutdown handler callables do not take any arguments, and may be either standard functions, or async functions.
+
lifespan - A lifespan context function, which can be used to perform startup and shutdown tasks. This is a newer style that replaces the on_startup and on_shutdown handlers. Use one or the other, not both.*
# Example post request with parameter that has a default value
+@app.post('/pet/dog')
+def pet_dog(dogname: str=None): return dogname
+
+# Working post request with optional parameter
+test_r(cli, '/pet/dog', '', 'post', data={})
@rt("/setcookie")
+def get(req): return cookie('now', datetime.now())
+
+@rt("/getcookie")
+def get(now:parsed_date): returnf'Cookie was set at time {now.time()}'
+
+print(cli.get('/setcookie').text)
+time.sleep(0.01)
+cli.get('/getcookie').text
' in txt and '' in txt
+
+@rt('/xt2')
+def get(): return H1('bar')
+
+txt = cli.get('/xt2').text
+assert 'FastHTML page' in txt and '
bar
' in txt and '' in txt
+
+assert cli.get('/xt2', **hxhdr).text.strip() == '
bar
'
+
+@rt('/xt3')
+def get(): return Html(Head(Title('hi')), Body(P('there')))
+
+txt = cli.get('/xt3').text
+assert 'FastHTML page' not in txt and 'hi' in txt and '
To expedite fast development, FastHTML comes with several built-in Javascript and formatting components. These are largely provided to demonstrate FastHTML JS patterns. There’s far too many JS libs for FastHTML to wrap them all, and as shown here the code to add FastHTML support is very simple anyway.
When an element is being dragged, this is the class used to distinguish it from the rest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/api/js.html.md b/api/js.html.md
new file mode 100644
index 00000000..6ae9d8d9
--- /dev/null
+++ b/api/js.html.md
@@ -0,0 +1,305 @@
+# Javascript examples
+
+
+
+
+To expedite fast development, FastHTML comes with several built-in
+Javascript and formatting components. These are largely provided to
+demonstrate FastHTML JS patterns. There’s far too many JS libs for
+FastHTML to wrap them all, and as shown here the code to add FastHTML
+support is very simple anyway.
+
+------------------------------------------------------------------------
+
+source
+
+### light_media
+
+> light_media (css:str)
+
+*Render light media for day mode views*
+
+
+
+
+
+
Type
+
Details
+
+
+
+
+
css
+
str
+
CSS to be included in the light media query
+
+
+
+
+``` python
+light_media('.body {color: green;}')
+```
+
+``` html
+
+```
+
+------------------------------------------------------------------------
+
+source
+
+### dark_media
+
+> dark_media (css:str)
+
+*Render dark media for nught mode views*
+
+
Start and stop a Jupyter compatible uvicorn server with ASGI app on port with log_level
+
Creating an object of this class also starts the Uvicorn server. It runs in a separate thread, so you can use normal HTTP client functions in a notebook.
Same as FastHTML, but with Jupyter compatible middleware and headers added
+
Instead of using the FastHTML class, use the FastJupy class. It’s a thin wrapper for FastHTML which adds the necessary headers and middleware required for Jupyter compatibility.
# Run the notebook locally to see the HTMX iframe in action
+# HTMX()
+
+# If only part of the page gets displayed, try tweaking the iframe_height parameter, for example:
+# HTMX(iframe_height="800px")
Generating a login link that sends the user to the OAuth provider is done with client.login_link().
+
It can sometimes be useful to pass state to the OAuth provider, so that when the user returns you can pick up where they left off. This can be done by passing the state parameter.
+
+
from fasthtml.common import*
+from fasthtml.jupyter import*
Call retr_info and then return id/subscriber value
+
After logging in via the provider, the user will be redirected back to the supplied redirect URL. The request to this URL will contain a code parameter, which is used to get an access token and fetch the user’s profile information. See the explanation here for a worked example. You can either:
+
+
Use client.retr_info(code) to get all the profile information, or
+
Use client.retr_id(code) to get just the user’s ID.
+
+
After either of these calls, you can also access the access token (used to revoke access, for example) with client.token["access_token"].
All our shapes just create regular FT elements. The only extra functionality provided by most of them is to add additional defined kwargs to improve auto-complete in IDEs and notebooks, and re-order parameters so that positional args can also be used to save a bit of typing, e.g:
Wraps an SVG shape as required for an HTMX OOB swap
+
When returning an SVG shape out-of-band (OOB) in HTMX, you need to wrap it with SvgOob to have it appear correctly. (SvgOob is just a shortcut for Template(Svg(...)), which is the trick that makes SVG OOB swaps work.)
Wraps an SVG shape as required for an HTMX inband swap
+
When returning an SVG shape in-band in HTMX, either have the calling element include hx_select='svg>*', or **svg_inb (which are two ways of saying the same thing), or wrap the response with SvgInb to have it appear correctly. (SvgInb is just a shortcut for the tuple (Svg(...), HtmxResponseHeaders(hx_reselect='svg>*')), which is the trick that makes SVG in-band swaps work.)
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/api/svg.html.md b/api/svg.html.md
new file mode 100644
index 00000000..78f68cf1
--- /dev/null
+++ b/api/svg.html.md
@@ -0,0 +1,639 @@
+# SVG
+
+
+
+
+``` python
+from nbdev.showdoc import show_doc
+```
+
+You can create SVGs directly from strings, for instance (as always, use
+`NotStr` or `Safe` to tell FastHTML to not escape the text):
+
+``` python
+svg = ''
+show(NotStr(svg))
+```
+
+
+
+You can also use libraries such as
+[fa6-icons](https://answerdotai.github.io/fa6-icons/).
+
+To create and modify SVGs using a Python API, use the FT elements in
+`fasthtml.svg`, discussed below.
+
+**Note**: `fasthtml.common` does NOT automatically export SVG elements.
+To get access to them, you need to import `fasthtml.svg` like so
+
+``` python
+from fasthtml.svg import *
+```
+
+------------------------------------------------------------------------
+
+source
+
+### Svg
+
+> Svg (*args, viewBox=None, h=None, w=None, height=None, width=None,
+> **kwargs)
+
+*An SVG tag; xmlns is added automatically, and viewBox defaults to
+height and width if not provided*
+
+To create your own SVGs, use `SVG`. It will automatically set the
+`viewBox` from height and width if not provided.
+
+All of our shapes will have some convenient kwargs added by using
+[`ft_svg`](https://AnswerDotAI.github.io/fasthtml/api/svg.html#ft_svg):
+
+------------------------------------------------------------------------
+
+source
+
+### ft_svg
+
+> ft_svg (tag:str, *c, transform=None, opacity=None, clip=None, mask=None,
+> filter=None, vector_effect=None, pointer_events=None,
+> target_id=None, hx_vals=None, id=None, cls=None, title=None,
+> style=None, accesskey=None, contenteditable=None, dir=None,
+> draggable=None, enterkeyhint=None, hidden=None, inert=None,
+> inputmode=None, lang=None, popover=None, spellcheck=None,
+> tabindex=None, translate=None, hx_get=None, hx_post=None,
+> hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,
+> hx_target=None, hx_swap=None, hx_swap_oob=None, hx_include=None,
+> hx_select=None, hx_select_oob=None, hx_indicator=None,
+> hx_push_url=None, hx_confirm=None, hx_disable=None,
+> hx_replace_url=None, hx_disabled_elt=None, hx_ext=None,
+> hx_headers=None, hx_history=None, hx_history_elt=None,
+> hx_inherit=None, hx_params=None, hx_preserve=None,
+> hx_prompt=None, hx_request=None, hx_sync=None, hx_validate=None)
+
+*Create a standard `FT` element with some SVG-specific attrs*
+
+## Basic shapes
+
+We’ll define a simple function to display SVG shapes in this notebook:
+
+``` python
+def demo(el, h=50, w=50): return show(Svg(h=h,w=w)(el))
+```
+
+------------------------------------------------------------------------
+
+source
+
+### Rect
+
+> Rect (width, height, x=0, y=0, fill=None, stroke=None, stroke_width=None,
+> rx=None, ry=None, transform=None, opacity=None, clip=None,
+> mask=None, filter=None, vector_effect=None, pointer_events=None,
+> target_id=None, hx_vals=None, id=None, cls=None, title=None,
+> style=None, accesskey=None, contenteditable=None, dir=None,
+> draggable=None, enterkeyhint=None, hidden=None, inert=None,
+> inputmode=None, lang=None, popover=None, spellcheck=None,
+> tabindex=None, translate=None, hx_get=None, hx_post=None,
+> hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,
+> hx_target=None, hx_swap=None, hx_swap_oob=None, hx_include=None,
+> hx_select=None, hx_select_oob=None, hx_indicator=None,
+> hx_push_url=None, hx_confirm=None, hx_disable=None,
+> hx_replace_url=None, hx_disabled_elt=None, hx_ext=None,
+> hx_headers=None, hx_history=None, hx_history_elt=None,
+> hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None,
+> hx_request=None, hx_sync=None, hx_validate=None)
+
+*A standard SVG `rect` element*
+
+All our shapes just create regular `FT` elements. The only extra
+functionality provided by most of them is to add additional defined
+kwargs to improve auto-complete in IDEs and notebooks, and re-order
+parameters so that positional args can also be used to save a bit of
+typing, e.g:
+
+``` python
+demo(Rect(30, 30, fill='blue', rx=8, ry=8))
+```
+
+
+
+------------------------------------------------------------------------
+
+source
+
+### Circle
+
+> Circle (r, cx=0, cy=0, fill=None, stroke=None, stroke_width=None,
+> transform=None, opacity=None, clip=None, mask=None, filter=None,
+> vector_effect=None, pointer_events=None, target_id=None,
+> hx_vals=None, id=None, cls=None, title=None, style=None,
+> accesskey=None, contenteditable=None, dir=None, draggable=None,
+> enterkeyhint=None, hidden=None, inert=None, inputmode=None,
+> lang=None, popover=None, spellcheck=None, tabindex=None,
+> translate=None, hx_get=None, hx_post=None, hx_put=None,
+> hx_delete=None, hx_patch=None, hx_trigger=None, hx_target=None,
+> hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,
+> hx_select_oob=None, hx_indicator=None, hx_push_url=None,
+> hx_confirm=None, hx_disable=None, hx_replace_url=None,
+> hx_disabled_elt=None, hx_ext=None, hx_headers=None,
+> hx_history=None, hx_history_elt=None, hx_inherit=None,
+> hx_params=None, hx_preserve=None, hx_prompt=None,
+> hx_request=None, hx_sync=None, hx_validate=None)
+
+*A standard SVG `circle` element*
+
+``` python
+demo(Circle(20, 25, 25, stroke='red', stroke_width=3))
+```
+
+
+
+------------------------------------------------------------------------
+
+source
+
+### Ellipse
+
+> Ellipse (rx, ry, cx=0, cy=0, fill=None, stroke=None, stroke_width=None,
+> transform=None, opacity=None, clip=None, mask=None, filter=None,
+> vector_effect=None, pointer_events=None, target_id=None,
+> hx_vals=None, id=None, cls=None, title=None, style=None,
+> accesskey=None, contenteditable=None, dir=None, draggable=None,
+> enterkeyhint=None, hidden=None, inert=None, inputmode=None,
+> lang=None, popover=None, spellcheck=None, tabindex=None,
+> translate=None, hx_get=None, hx_post=None, hx_put=None,
+> hx_delete=None, hx_patch=None, hx_trigger=None, hx_target=None,
+> hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,
+> hx_select_oob=None, hx_indicator=None, hx_push_url=None,
+> hx_confirm=None, hx_disable=None, hx_replace_url=None,
+> hx_disabled_elt=None, hx_ext=None, hx_headers=None,
+> hx_history=None, hx_history_elt=None, hx_inherit=None,
+> hx_params=None, hx_preserve=None, hx_prompt=None,
+> hx_request=None, hx_sync=None, hx_validate=None)
+
+*A standard SVG `ellipse` element*
+
+``` python
+demo(Ellipse(20, 10, 25, 25))
+```
+
+
+
+------------------------------------------------------------------------
+
+source
+
+### transformd
+
+> transformd (translate=None, scale=None, rotate=None, skewX=None,
+> skewY=None, matrix=None)
+
+*Create an SVG `transform` kwarg dict*
+
+``` python
+rot = transformd(rotate=(45, 25, 25))
+rot
+```
+
+ {'transform': 'rotate(45,25,25)'}
+
+``` python
+demo(Ellipse(20, 10, 25, 25, **rot))
+```
+
+
+
+------------------------------------------------------------------------
+
+source
+
+### Line
+
+> Line (x1, y1, x2=0, y2=0, stroke='black', w=None, stroke_width=1,
+> transform=None, opacity=None, clip=None, mask=None, filter=None,
+> vector_effect=None, pointer_events=None, target_id=None,
+> hx_vals=None, id=None, cls=None, title=None, style=None,
+> accesskey=None, contenteditable=None, dir=None, draggable=None,
+> enterkeyhint=None, hidden=None, inert=None, inputmode=None,
+> lang=None, popover=None, spellcheck=None, tabindex=None,
+> translate=None, hx_get=None, hx_post=None, hx_put=None,
+> hx_delete=None, hx_patch=None, hx_trigger=None, hx_target=None,
+> hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,
+> hx_select_oob=None, hx_indicator=None, hx_push_url=None,
+> hx_confirm=None, hx_disable=None, hx_replace_url=None,
+> hx_disabled_elt=None, hx_ext=None, hx_headers=None,
+> hx_history=None, hx_history_elt=None, hx_inherit=None,
+> hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,
+> hx_sync=None, hx_validate=None)
+
+*A standard SVG `line` element*
+
+``` python
+demo(Line(20, 30, w=3))
+```
+
+
+
+------------------------------------------------------------------------
+
+source
+
+### Polyline
+
+> Polyline (*args, points=None, fill=None, stroke=None, stroke_width=None,
+> transform=None, opacity=None, clip=None, mask=None,
+> filter=None, vector_effect=None, pointer_events=None,
+> target_id=None, hx_vals=None, id=None, cls=None, title=None,
+> style=None, accesskey=None, contenteditable=None, dir=None,
+> draggable=None, enterkeyhint=None, hidden=None, inert=None,
+> inputmode=None, lang=None, popover=None, spellcheck=None,
+> tabindex=None, translate=None, hx_get=None, hx_post=None,
+> hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,
+> hx_target=None, hx_swap=None, hx_swap_oob=None,
+> hx_include=None, hx_select=None, hx_select_oob=None,
+> hx_indicator=None, hx_push_url=None, hx_confirm=None,
+> hx_disable=None, hx_replace_url=None, hx_disabled_elt=None,
+> hx_ext=None, hx_headers=None, hx_history=None,
+> hx_history_elt=None, hx_inherit=None, hx_params=None,
+> hx_preserve=None, hx_prompt=None, hx_request=None,
+> hx_sync=None, hx_validate=None)
+
+*A standard SVG `polyline` element*
+
+``` python
+demo(Polyline((0,0), (10,10), (20,0), (30,10), (40,0),
+ fill='yellow', stroke='blue', stroke_width=2))
+```
+
+
+
+``` python
+demo(Polyline(points='0,0 10,10 20,0 30,10 40,0', fill='purple', stroke_width=2))
+```
+
+
+
+------------------------------------------------------------------------
+
+source
+
+### Polygon
+
+> Polygon (*args, points=None, fill=None, stroke=None, stroke_width=None,
+> transform=None, opacity=None, clip=None, mask=None, filter=None,
+> vector_effect=None, pointer_events=None, target_id=None,
+> hx_vals=None, id=None, cls=None, title=None, style=None,
+> accesskey=None, contenteditable=None, dir=None, draggable=None,
+> enterkeyhint=None, hidden=None, inert=None, inputmode=None,
+> lang=None, popover=None, spellcheck=None, tabindex=None,
+> translate=None, hx_get=None, hx_post=None, hx_put=None,
+> hx_delete=None, hx_patch=None, hx_trigger=None, hx_target=None,
+> hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,
+> hx_select_oob=None, hx_indicator=None, hx_push_url=None,
+> hx_confirm=None, hx_disable=None, hx_replace_url=None,
+> hx_disabled_elt=None, hx_ext=None, hx_headers=None,
+> hx_history=None, hx_history_elt=None, hx_inherit=None,
+> hx_params=None, hx_preserve=None, hx_prompt=None,
+> hx_request=None, hx_sync=None, hx_validate=None)
+
+*A standard SVG `polygon` element*
+
+``` python
+demo(Polygon((25,5), (43.3,15), (43.3,35), (25,45), (6.7,35), (6.7,15),
+ fill='lightblue', stroke='navy', stroke_width=2))
+```
+
+
+
+``` python
+demo(Polygon(points='25,5 43.3,15 43.3,35 25,45 6.7,35 6.7,15',
+ fill='lightgreen', stroke='darkgreen', stroke_width=2))
+```
+
+
+
+------------------------------------------------------------------------
+
+source
+
+### Text
+
+> Text (*args, x=0, y=0, font_family=None, font_size=None, fill=None,
+> text_anchor=None, dominant_baseline=None, font_weight=None,
+> font_style=None, text_decoration=None, transform=None,
+> opacity=None, clip=None, mask=None, filter=None,
+> vector_effect=None, pointer_events=None, target_id=None,
+> hx_vals=None, id=None, cls=None, title=None, style=None,
+> accesskey=None, contenteditable=None, dir=None, draggable=None,
+> enterkeyhint=None, hidden=None, inert=None, inputmode=None,
+> lang=None, popover=None, spellcheck=None, tabindex=None,
+> translate=None, hx_get=None, hx_post=None, hx_put=None,
+> hx_delete=None, hx_patch=None, hx_trigger=None, hx_target=None,
+> hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,
+> hx_select_oob=None, hx_indicator=None, hx_push_url=None,
+> hx_confirm=None, hx_disable=None, hx_replace_url=None,
+> hx_disabled_elt=None, hx_ext=None, hx_headers=None,
+> hx_history=None, hx_history_elt=None, hx_inherit=None,
+> hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,
+> hx_sync=None, hx_validate=None)
+
+*A standard SVG `text` element*
+
+``` python
+demo(Text("Hello!", x=10, y=30))
+```
+
+
+
+## Paths
+
+Paths in SVGs are more complex, so we add a small (optional) fluent
+interface for constructing them:
+
+------------------------------------------------------------------------
+
+source
+
+### PathFT
+
+> PathFT (tag:str, cs:tuple, attrs:dict=None, void_=False, **kwargs)
+
+*A ‘Fast Tag’ structure, containing `tag`,`children`,and `attrs`*
+
+------------------------------------------------------------------------
+
+source
+
+### Path
+
+> Path (d='', fill=None, stroke=None, stroke_width=None, transform=None,
+> opacity=None, clip=None, mask=None, filter=None,
+> vector_effect=None, pointer_events=None, target_id=None,
+> hx_vals=None, id=None, cls=None, title=None, style=None,
+> accesskey=None, contenteditable=None, dir=None, draggable=None,
+> enterkeyhint=None, hidden=None, inert=None, inputmode=None,
+> lang=None, popover=None, spellcheck=None, tabindex=None,
+> translate=None, hx_get=None, hx_post=None, hx_put=None,
+> hx_delete=None, hx_patch=None, hx_trigger=None, hx_target=None,
+> hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,
+> hx_select_oob=None, hx_indicator=None, hx_push_url=None,
+> hx_confirm=None, hx_disable=None, hx_replace_url=None,
+> hx_disabled_elt=None, hx_ext=None, hx_headers=None,
+> hx_history=None, hx_history_elt=None, hx_inherit=None,
+> hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,
+> hx_sync=None, hx_validate=None)
+
+*Create a standard `path` SVG element. This is a special object*
+
+Let’s create a square shape, but using
+[`Path`](https://AnswerDotAI.github.io/fasthtml/api/svg.html#path)
+instead of
+[`Rect`](https://AnswerDotAI.github.io/fasthtml/api/svg.html#rect):
+
+- M(10, 10): Move to starting point (10, 10)
+- L(40, 10): Line to (40, 10) - top edge
+- L(40, 40): Line to (40, 40) - right edge
+- L(10, 40): Line to (10, 40) - bottom edge
+- Z(): Close path - connects back to start
+
+M = Move to, L = Line to, Z = Close path
+
+``` python
+demo(Path(fill='none', stroke='purple', stroke_width=2
+ ).M(10, 10).L(40, 10).L(40, 40).L(10, 40).Z())
+```
+
+
+
+Using curves we can create a spiral:
+
+``` python
+p = (Path(fill='none', stroke='purple', stroke_width=2)
+ .M(25, 25)
+ .C(25, 25, 20, 20, 30, 20)
+ .C(40, 20, 40, 30, 30, 30)
+ .C(20, 30, 20, 15, 35, 15)
+ .C(50, 15, 50, 35, 25, 35)
+ .C(0, 35, 0, 10, 40, 10)
+ .C(80, 10, 80, 40, 25, 40))
+demo(p, 50, 100)
+```
+
+
+
+Using arcs and curves we can create a map marker icon:
+
+``` python
+p = (Path(fill='red')
+ .M(25,45)
+ .C(25,45,10,35,10,25)
+ .A(15,15,0,1,1,40,25)
+ .C(40,35,25,45,25,45)
+ .Z())
+demo(p)
+```
+
+
+
+Behind the scenes it’s just creating regular SVG path `d` attr – you can
+pass `d` in directly if you prefer.
+
+``` python
+print(p.d)
+```
+
+ M25 45 C25 45 10 35 10 25 A15 15 0 1 1 40 25 C40 35 25 45 25 45 Z
+
+``` python
+demo(Path(d='M25 45 C25 45 10 35 10 25 A15 15 0 1 1 40 25 C40 35 25 45 25 45 Z'))
+```
+
+
+
+------------------------------------------------------------------------
+
+source
+
+### PathFT.M
+
+> PathFT.M (x, y)
+
+*Move to.*
+
+------------------------------------------------------------------------
+
+source
+
+### PathFT.L
+
+> PathFT.L (x, y)
+
+*Line to.*
+
+------------------------------------------------------------------------
+
+source
+
+### PathFT.H
+
+> PathFT.H (x)
+
+*Horizontal line to.*
+
+------------------------------------------------------------------------
+
+source
+
+### PathFT.V
+
+> PathFT.V (y)
+
+*Vertical line to.*
+
+------------------------------------------------------------------------
+
+source
+
+### PathFT.Z
+
+> PathFT.Z ()
+
+*Close path.*
+
+------------------------------------------------------------------------
+
+source
+
+### PathFT.C
+
+> PathFT.C (x1, y1, x2, y2, x, y)
+
+*Cubic Bézier curve.*
+
+------------------------------------------------------------------------
+
+source
+
+### PathFT.S
+
+> PathFT.S (x2, y2, x, y)
+
+*Smooth cubic Bézier curve.*
+
+------------------------------------------------------------------------
+
+source
+
+### PathFT.Q
+
+> PathFT.Q (x1, y1, x, y)
+
+*Quadratic Bézier curve.*
+
+------------------------------------------------------------------------
+
+source
+
+### PathFT.T
+
+> PathFT.T (x, y)
+
+*Smooth quadratic Bézier curve.*
+
+------------------------------------------------------------------------
+
+source
+
+### PathFT.A
+
+> PathFT.A (rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, x, y)
+
+*Elliptical Arc.*
+
+## HTMX helpers
+
+------------------------------------------------------------------------
+
+source
+
+### SvgOob
+
+> SvgOob (*args, **kwargs)
+
+*Wraps an SVG shape as required for an HTMX OOB swap*
+
+When returning an SVG shape out-of-band (OOB) in HTMX, you need to wrap
+it with
+[`SvgOob`](https://AnswerDotAI.github.io/fasthtml/api/svg.html#svgoob)
+to have it appear correctly.
+([`SvgOob`](https://AnswerDotAI.github.io/fasthtml/api/svg.html#svgoob)
+is just a shortcut for `Template(Svg(...))`, which is the trick that
+makes SVG OOB swaps work.)
+
+------------------------------------------------------------------------
+
+source
+
+### SvgInb
+
+> SvgInb (*args, **kwargs)
+
+*Wraps an SVG shape as required for an HTMX inband swap*
+
+When returning an SVG shape in-band in HTMX, either have the calling
+element include `hx_select='svg>*'`, or `**svg_inb` (which are two ways
+of saying the same thing), or wrap the response with
+[`SvgInb`](https://AnswerDotAI.github.io/fasthtml/api/svg.html#svginb)
+to have it appear correctly.
+([`SvgInb`](https://AnswerDotAI.github.io/fasthtml/api/svg.html#svginb)
+is just a shortcut for the tuple
+`(Svg(...), HtmxResponseHeaders(hx_reselect='svg>*'))`, which is the
+trick that makes SVG in-band swaps work.)
diff --git a/api/xtend.html b/api/xtend.html
new file mode 100644
index 00000000..ec4ece19
--- /dev/null
+++ b/api/xtend.html
@@ -0,0 +1,1227 @@
+
+
+
+
+
+
+
+
+
+
+Component extensions – fasthtml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
FT, or ‘FastTags’, are the display components of FastHTML. In fact, the word “components” in the context of FastHTML is often synonymous with FT.
+
For example, when we look at a FastHTML app, in particular the views, as well as various functions and other objects, we see something like the code snippet below. It’s the return statement that we want to pay attention to:
+
+
from fasthtml.common import*
+
+def example():
+# The code below is a set of ft components
+return Div(
+ H1("FastHTML APP"),
+ P("Let's do this"),
+ cls="go"
+ )
+
+
Let’s go ahead and call our function and print the result:
+
+
example()
+
+
<div class="go">
+ <h1>FastHTML APP</h1>
+ <p>Let's do this</p>
+</div>
+
+
+
As you can see, when returned to the user from a Python callable, like a function, the ft components are transformed into their string representations of XML or XML-like content such as HTML. More concisely, ft turns Python objects into HTML.
+
Now that we know what ft components look and behave like we can begin to understand them. At their most fundamental level, ft components:
+
+
Are Python callables, specifically functions, classes, methods of classes, lambda functions, and anything else called with parenthesis that returns a value.
+
Return a sequence of values which has three elements:
+
+
The tag to be generated
+
The content of the tag, which is a tuple of strings/tuples. If a tuple, it is the three-element structure of an ft component
+
A dictionary of XML attributes and their values
+
+
FastHTML’s default ft components words begin with an uppercase letter. Examples include Title(), Ul(), and Div() Custom components have included things like BlogPost and CityMap.
+
+
+
How FastHTML names ft components
+
When it comes to naming ft components, FastHTML appears to break from PEP8. Specifically, PEP8 specifies that when naming variables, functions and instantiated classes we use the snake_case_pattern. That is to say, lowercase with words separated by underscores. However, FastHTML uses PascalCase for ft components.
+
There’s a couple of reasons for this:
+
+
ft components can be made from any callable type, so adhering to any one pattern doesn’t make much sense
+
It makes for easier reading of FastHTML code, as anything that is PascalCase is probably an ft component
+
+
+
+
Default FT components
+
FastHTML has over 150 FT components designed to accelerate web development. Most of these mirror HTML tags such as <div>, <p>, <a>, <title>, and more. However, there are some extra tags added, including:
+
+
Titled, a combination of the Title() and H1() tags
Some people prefer to write code using namespaces while adhering to PEP8. If that’s a preference, projects can be coded using the fasthtml.ft namespace.
+
+
from fasthtml import ft
+
+ft.Ul(
+ ft.Li("one"),
+ ft.Li("two"),
+ ft.Li("three")
+)
+Line 2 demonstrates that FastHTML appreciates Labels surrounding their fields.
+
+
2
+
+On line 5, we can see that attributes set to the boolean value of True are rendered with just the name of the attribute.
+
+
3
+
+On line 6, we demonstrate that attributes set to the boolean value of False do not appear in the rendered output.
+
+
4
+
+Line 7 is an example of how integers and other non-string values in the rendered output are converted to strings.
+
+
5
+
+Line 8 is where we set the HTML class using the cls argument. We use cls here as class is a reserved word in Python. During the rendering process this will be converted to the word “class”.
+
+
6
+
+Line 9 demonstrates that any named argument passed into an ft component will have the leading underscore stripped away before rendering. Useful for handling reserved words in Python.
+
+
7
+
+On line 10 we have an attribute name that cannot be represented as a python variable. In cases like these, we can use an unpacked dict to represent these values.
+
+
8
+
+The use of _for on line 12 is another demonstration of an argument having the leading underscore stripped during render. We can also use fr as that will be expanded to for.
+
It is possible and sometimes useful to create your own ft components that generate non-standard tags that are not in the FastHTML library. FastHTML supports created and defining those new tags flexibly.
If you use type hints, we strongly suggest that FT components be treated as the Any type.
+
The reason is that FastHTML leverages python’s dynamic features to a great degree. Especially when it comes to FT components, which can evaluate out to be FT|str|None|tuple as well as anything that supports the __ft__, __html__, and __str__ method. That’s enough of the Python stack that assigning anything but Any to be the FT type will prove an exercise in frustation.
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/explains/explaining_xt_components.html.md b/explains/explaining_xt_components.html.md
new file mode 100644
index 00000000..2005877a
--- /dev/null
+++ b/explains/explaining_xt_components.html.md
@@ -0,0 +1,215 @@
+# **FT** Components
+
+
+
+
+**FT**, or ‘FastTags’, are the display components of FastHTML. In fact,
+the word “components” in the context of FastHTML is often synonymous
+with **FT**.
+
+For example, when we look at a FastHTML app, in particular the views, as
+well as various functions and other objects, we see something like the
+code snippet below. It’s the `return` statement that we want to pay
+attention to:
+
+``` python
+from fasthtml.common import *
+
+def example():
+ # The code below is a set of ft components
+ return Div(
+ H1("FastHTML APP"),
+ P("Let's do this"),
+ cls="go"
+ )
+```
+
+Let’s go ahead and call our function and print the result:
+
+``` python
+example()
+```
+
+``` xml
+
+
FastHTML APP
+
Let's do this
+
+```
+
+As you can see, when returned to the user from a Python callable, like a
+function, the ft components are transformed into their string
+representations of XML or XML-like content such as HTML. More concisely,
+*ft turns Python objects into HTML*.
+
+Now that we know what ft components look and behave like we can begin to
+understand them. At their most fundamental level, ft components:
+
+1. Are Python callables, specifically functions, classes, methods of
+ classes, lambda functions, and anything else called with parenthesis
+ that returns a value.
+2. Return a sequence of values which has three elements:
+ 1. The tag to be generated
+ 2. The content of the tag, which is a tuple of strings/tuples. If a
+ tuple, it is the three-element structure of an ft component
+ 3. A dictionary of XML attributes and their values
+3. FastHTML’s default ft components words begin with an uppercase
+ letter. Examples include `Title()`, `Ul()`, and `Div()` Custom
+ components have included things like `BlogPost` and `CityMap`.
+
+## How FastHTML names ft components
+
+When it comes to naming ft components, FastHTML appears to break from
+PEP8. Specifically, PEP8 specifies that when naming variables, functions
+and instantiated classes we use the `snake_case_pattern`. That is to
+say, lowercase with words separated by underscores. However, FastHTML
+uses `PascalCase` for ft components.
+
+There’s a couple of reasons for this:
+
+1. ft components can be made from any callable type, so adhering to any
+ one pattern doesn’t make much sense
+2. It makes for easier reading of FastHTML code, as anything that is
+ PascalCase is probably an ft component
+
+## Default **FT** components
+
+FastHTML has over 150 **FT** components designed to accelerate web
+development. Most of these mirror HTML tags such as `
Why does my editor say that I have errors in my FastHTML code?
+
Many editors, including Visual Studio Code, use PyLance to provide error checking for Python. However, PyLance’s error checking is just a guess – it can’t actually know whether your code is correct or not. PyLance particularly struggles with FastHTML’s syntax, which leads to it often reporting false error messages in FastHTML projects.
+
To avoid these misleading error messages, it’s best to disable some PyLance error checking in your FastHTML projects. Here’s how to do it in Visual Studio Code (the same approach should also work in other editors based on vscode, such as Cursor and GitHub Codespaces):
+
+
Open your FastHTML project
+
Press Ctrl+Shift+P (or Cmd+Shift+P on Mac) to open the Command Palette
+
Type “Preferences: Open Workspace Settings (JSON)” and select it
+
In the JSON file that opens, add the following lines:
Even with PyLance diagnostics turned off, your FastHTML code will still run correctly. If you’re still seeing some false errors from PyLance, you can disable it entirely by adding this to your settings:
If you are coming from a data science background the fastai coding style may already be your preferred style.
+
If you are coming from a PEP-8 background where the use of ruff is encouraged, there is a learning curve. However, once you get used to the fastai coding style you may discover yourself appreciating the concise nature of this style. It also encourages using more functional programming tooling, which is both productive and fun. Having said that, it’s entirely optional!
+
+
+
Why not JSX?
+
Many have asked! We think there’s no benefit… Python’s positional and kw args precisely 1:1 map already to html/xml children and attrs, so there’s no need for a new syntax.
+
We wrote some more thoughts on Why Python HTML components over Jinja2, Mako, or JSX here.
+
+
+
Why use import *
+
First, through the use of the __all__ attribute in our Python modules we control what actually gets imported. So there’s no risk of namespace pollution.
+
Second, our style lends itself to working in rather compact Jupyter notebooks and small Python modules. Hence we know about the source code whose libraries we import * from. This terseness means we can develop faster. We’re a small team, and any edge we can gain is important to us.
+
Third, for external libraries, be it core Python, SQLAlchemy, or other things we do tend to use explicit imports. In part to avoid namespace collisions, and also as reference to know where things are coming from.
+
We’ll finish by saying a lot of our users employ explicit imports. If that’s the path you want to take, we encourage the use of from fasthtml import common as fh. The acronym of fh makes it easy to recognize that a symbol is from the FastHTML library.
+
+
+
Can FastHTML be used for dashboards?
+
Yes it can. In fact, it excels at building dashboards. In addition to being great for building static dashboards, because of its foundation in ASGI and tech stack, FastHTML natively supports Websockets. That means using FastHTML we can create dashboards that autoupdate.
+
+
+
Why is FastHTML developed using notebooks?
+
Some people are under the impression that writing software in notebooks is bad.
+
Watch this video. We’ve used Jupyter notebooks exported via nbdev to write a wide range of “very serious” software projects over the last three years. This includes deep learning libraries, API clients, Python language extensions, terminal user interfaces, web frameworks, and more!
+
nbdev is a Jupyter-powered tool for writing software. Traditional programming environments throw away the result of your exploration in REPLs or notebooks. nbdev makes exploration an integral part of your workflow, all while promoting software engineering best practices.
+
+
+
Why not pyproject.toml for packaging?
+
FastHTML uses a setup.py module instead of a pyproject.toml file to configure itself for installation. The reason for this is pyproject.toml is not compatible with nbdev, which is what is used to write and build FastHTML.
+
The nbdev project spent around a year trying to move to pyproject.toml but there was insufficient functionality in the toml-based approach to complete the transition.
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/explains/faq.html.md b/explains/faq.html.md
new file mode 100644
index 00000000..bab46e9d
--- /dev/null
+++ b/explains/faq.html.md
@@ -0,0 +1,133 @@
+# FAQ
+
+
+
+
+## Why does my editor say that I have errors in my FastHTML code?
+
+Many editors, including Visual Studio Code, use PyLance to provide error
+checking for Python. However, PyLance’s error checking is just a guess –
+it can’t actually know whether your code is correct or not. PyLance
+particularly struggles with FastHTML’s syntax, which leads to it often
+reporting false error messages in FastHTML projects.
+
+To avoid these misleading error messages, it’s best to disable some
+PyLance error checking in your FastHTML projects. Here’s how to do it in
+Visual Studio Code (the same approach should also work in other editors
+based on vscode, such as Cursor and GitHub Codespaces):
+
+1. Open your FastHTML project
+2. Press `Ctrl+Shift+P` (or `Cmd+Shift+P` on Mac) to open the Command
+ Palette
+3. Type “Preferences: Open Workspace Settings (JSON)” and select it
+4. In the JSON file that opens, add the following lines:
+
+``` json
+{
+ "python.analysis.diagnosticSeverityOverrides": {
+ "reportGeneralTypeIssues": "none",
+ "reportOptionalMemberAccess": "none",
+ "reportWildcardImportFromLibrary": "none",
+ "reportRedeclaration": "none",
+ "reportAttributeAccessIssue": "none",
+ "reportInvalidTypeForm": "none",
+ "reportAssignmentType": "none",
+ }
+}
+```
+
+5. Save the file
+
+Even with PyLance diagnostics turned off, your FastHTML code will still
+run correctly. If you’re still seeing some false errors from PyLance,
+you can disable it entirely by adding this to your settings:
+
+``` json
+{
+ "python.analysis.ignore": [ "*" ]
+}
+```
+
+## Why the distinctive coding style?
+
+FastHTML coding style is the [fastai coding
+style](https://docs.fast.ai/dev/style.html).
+
+If you are coming from a data science background the **fastai coding
+style** may already be your preferred style.
+
+If you are coming from a PEP-8 background where the use of ruff is
+encouraged, there is a learning curve. However, once you get used to the
+**fastai coding style** you may discover yourself appreciating the
+concise nature of this style. It also encourages using more functional
+programming tooling, which is both productive and fun. Having said that,
+it’s entirely optional!
+
+## Why not JSX?
+
+Many have asked! We think there’s no benefit… Python’s positional and kw
+args precisely 1:1 map already to html/xml children and attrs, so
+there’s no need for a new syntax.
+
+We wrote some more thoughts on Why Python HTML components over Jinja2,
+Mako, or JSX
+[here](https://www.answer.ai/posts/2024-08-03-fasthtml.html#why).
+
+## Why use `import *`
+
+First, through the use of the
+[`__all__`](https://docs.python.org/3/tutorial/modules.html#importing-from-a-package)
+attribute in our Python modules we control what actually gets imported.
+So there’s no risk of namespace pollution.
+
+Second, our style lends itself to working in rather compact Jupyter
+notebooks and small Python modules. Hence we know about the source code
+whose libraries we `import *` from. This terseness means we can develop
+faster. We’re a small team, and any edge we can gain is important to us.
+
+Third, for external libraries, be it core Python, SQLAlchemy, or other
+things we do tend to use explicit imports. In part to avoid namespace
+collisions, and also as reference to know where things are coming from.
+
+We’ll finish by saying a lot of our users employ explicit imports. If
+that’s the path you want to take, we encourage the use of
+`from fasthtml import common as fh`. The acronym of `fh` makes it easy
+to recognize that a symbol is from the FastHTML library.
+
+## Can FastHTML be used for dashboards?
+
+Yes it can. In fact, it excels at building dashboards. In addition to
+being great for building static dashboards, because of its
+[foundation](https://about.fastht.ml/foundation) in ASGI and [tech
+stack](https://about.fastht.ml/tech), FastHTML natively supports
+Websockets. That means using FastHTML we can create dashboards that
+autoupdate.
+
+## Why is FastHTML developed using notebooks?
+
+Some people are under the impression that writing software in notebooks
+is bad.
+
+[Watch this
+video](https://www.youtube.com/watch?v=9Q6sLbz37gk&ab_channel=JeremyHoward).
+We’ve used Jupyter notebooks exported via `nbdev` to write a wide range
+of “very serious” software projects over the last three years. This
+includes deep learning libraries, API clients, Python language
+extensions, terminal user interfaces, web frameworks, and more!
+
+[nbdev](https://nbdev.fast.ai/) is a Jupyter-powered tool for writing
+software. Traditional programming environments throw away the result of
+your exploration in REPLs or notebooks. `nbdev` makes exploration an
+integral part of your workflow, all while promoting software engineering
+best practices.
+
+## Why not pyproject.toml for packaging?
+
+FastHTML uses a `setup.py` module instead of a `pyproject.toml` file to
+configure itself for installation. The reason for this is
+`pyproject.toml` is not compatible with [nbdev](https://nbdev.fast.ai/),
+which is what is used to write and build FastHTML.
+
+The nbdev project spent around a year trying to move to pyproject.toml
+but there was insufficient functionality in the toml-based approach to
+complete the transition.
diff --git a/explains/imgs/gh-oauth.png b/explains/imgs/gh-oauth.png
new file mode 100644
index 00000000..6d3758ac
Binary files /dev/null and b/explains/imgs/gh-oauth.png differ
diff --git a/explains/minidataapi.html b/explains/minidataapi.html
new file mode 100644
index 00000000..8ba94a81
--- /dev/null
+++ b/explains/minidataapi.html
@@ -0,0 +1,1376 @@
+
+
+
+
+
+
+
+
+
+MiniDataAPI Spec – fasthtml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
The MiniDataAPI is a persistence API specification that designed to be small and relatively easy to implement across a wide range of datastores. While early implementations have been SQL-based, the specification can be quickly implemented in key/value stores, document databases, and more.
+
+
+
+
+
+
+Work in Progress
+
+
+
+
The MiniData API spec is a work in progress, subject to change. While the majority of design is complete, expect there could be breaking changes.
+
+
+
+
Why?
+
The MiniDataAPI specification allows us to use the same API for many different database engines. Any application using the MiniDataAPI spec for interacting with its database requires no modification beyond import and configuration changes to switch database engines. For example, to convert an application from Fastlite running SQLite to FastSQL running PostgreSQL, should require only changing these two lines:
+
+
+
FastLite version
+
from fastlite import*
+db = Database('test.db')
+
+
FastSQL version
+
from fastsql import*
+db = Database('postgres:...')
+
+
+
As both libraries adhere to the MiniDataAPI specification, the rest of the code in the application should remain the same. The advantage of the MiniDataAPI spec is that it allows people to use whatever datastores they have access to or prefer.
+
+
+
+
+
+
+Note
+
+
+
+
Switching databases won’t migrate any existing data between databases.
+
+
+
+
Easy to learn, quick to implement
+
The MiniDataAPI specification is designed to be easy-to-learn and quick to implement. It focuses on straightforward Create, Read, Update, and Delete (CRUD) operations.
+
MiniDataAPI databases aren’t limited to just row-based systems. In fact, the specification is closer in design to a key/value store than a set of records. What’s exciting about this is we can write implementations for tools like Python dict stored as JSON, Redis, and even the venerable ZODB.
+
+
+
Limitations of the MiniDataAPI Specification
+
+
“Mini refers to the lightweightness of specification, not the data.”
+
– Jeremy Howard
+
+
The advantages of the MiniDataAPI come at a cost. The MiniDataAPI specification focuses a very small set of features compared to what can be found in full-fledged ORMs and query languages. It intentionally avoids nuances or sophisticated features.
+
This means the specification does not include joins or formal foreign keys. Complex data stored over multiple tables that require joins isn’t handled well. For this kind of scenario it’s probably for the best to use more sophisticated ORMs or even direct database queries.
+
+
+
Summary of the MiniDataAPI Design
+
+
Easy-to-learn
+
Relative quick to implement for new database engines
+
An API for CRUD operations
+
For many different types of databases including row- and key/value-based designs
+
Intentionally small in terms of features: no joins, no foreign keys, no database specific features
+
Best for simpler designs, complex architectures will need more sophisticated tools.
+
+
+
+
+
Connect/construct the database
+
We connect or construct the database by passing in a string connecting to the database endpoint or a filepath representing the database’s location. While this example is for SQLite running in memory, other databases such as PostgreSQL, Redis, MongoDB, might instead use a URI pointing at the database’s filepath or endpoint. The method of connecting to a DB is not part of this API, but part of the underlying library. For instance, for fastlite:
+
+
db = database(':memory:')
+
+
Here’s a complete list of the available methods in the API, all documented below (assuming db is a database and t is a table):
+
+
db.create
+
t.insert
+
t.delete
+
t.update
+
t[key]
+
t(...)
+
t.xtra
+
+
+
+
Tables
+
For the sake of expediency, this document uses a SQL example. However, tables can represent anything, not just the fundamental construct of a SQL databases. They might represent keys within a key/value structure or files on a hard-drive.
+
+
Creating tables
+
We use a create() method attached to Database object (db in our example) to create the tables.
If no pk is provided, id is assumed to be the primary key. Regardless of whether you mark a class as a dataclass or not, it will be turned into one – specifically into a flexiclass.
The MiniData API spec supports compound primary keys, where more than one column is used to identify records. We’ll also use this example to demonstrate creating a table using a dict of keyword arguments.
Depending on the database type, this method can include transforms - the ability to modify the tables. Let’s go ahead and add a password field for our table called pwd.
The specification is designed to provide as straightforward CRUD API (Create, Read, Update, and Delete) as possible. Additional features like joins are out of scope.
+
+
.insert()
+
Add a new record to the database. We want to support as many types as possible, for now we have tests for Python classes, dataclasses, and dicts. Returns an instance of the new record.
Update an existing record of the database. Must accept Python dict, dataclasses, and standard classes. Uses the primary key for identifying the record to be changed. Returns an instance of the updated record.
If the primary key doesn’t match a record, raise a NotFoundError.
+
John hasn’t started with us yet so doesn’t get the chance yet to travel in time.
+
+
try: users.update(User(name='John', year_started=2024, email='j@example.com'))
+except NotFoundError: print('User not found')
+
+
User not found
+
+
+
+
+
.delete()
+
Delete a record of the database. Uses the primary key for identifying the record to be removed. Returns a table object.
+
Charlie decides to not travel in time. He exits our little group.
+
+
users.delete('Charlie')
+
+
<Table user (name, email, year_started, pwd)>
+
+
+
If the primary key value can’t be found, raises a NotFoundError.
+
+
try: users.delete('Charlies')
+except NotFoundError: print('User not found')
+
+
User not found
+
+
+
In John’s case, he isn’t time travelling with us yet so can’t be removed.
+
+
try: users.delete('John')
+except NotFoundError: print('User not found')
+
+
User not found
+
+
+
Deleting records with compound primary keys requires providing the entire key.
+
+
publications.delete(['Alma' , 2035])
+
+
<Table publication (authors, year, title)>
+
+
+
+
+
in keyword
+
Are Alma and John contained in the Users table? Or, to be technically precise, is the item with the specified primary key value in this table?
+
+
'Alma'in users, 'John'in users
+
+
(True, False)
+
+
+
Also works with compound primary keys, as shown below. You’ll note that the operation can be done with either a list or tuple.
+
+
['Alma', 2019] in publications
+
+
True
+
+
+
And now for a False result, where John has no publications.
+
+
('John', 1967) in publications
+
+
False
+
+
+
+
+
.xtra()
+
If we set fields within the .xtra function to a particular value, then indexing is also filtered by those. This applies to every database method except for record creation. This makes it easier to limit users (or other objects) access to only things for which they have permission.
+
For example, if we query all our records below without setting values v ia the .xtra function, we can see todos for everyone. Pay special attention to the id values of all three records, as we are about to filter most of them away.
+
+
todos()
+
+
[Todo(id=1, title='Write MiniDataAPI spec', detail=None, status='open', name='Braden'),
+ Todo(id=2, title='Implement SSE in FastHTML', detail=None, status='open', name='Alma'),
+ Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')]
+
+
+
Let’s use .xtra to constrain results just to Charlie. We set the name field in Todos, but it could be any field defined for this table.
+
+
todos.xtra(name='Charlie')
+
+
We’ve now set a field to a value with .xtra, if we loop over all the records again, only those assigned to records with a name of Charlie will be displayed.
+
+
todos()
+
+
[Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')]
+
+
+
The in keyword is also affected. Only records with a name of Charlie will evaluate to be True. Let’s demonstrate by testing it with a Charlie record:
+
+
ct = todos[3]
+ct
+
+
Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')
+
+
+
Charlie’s record has an ID of 3. Here we demonstrate that Charlie’s TODO can be found in the list of todos:
+
+
ct.idin todos
+
+
True
+
+
+
If we try in with the other IDs the query fails because the filtering is now set to just records with a name of Charlie.
+
+
1in todos, 2in todos
+
+
(False, False)
+
+
+
+
try: todos[2]
+except NotFoundError: print('Record not found')
+
+
Record not found
+
+
+
We are also constrained by what records we can update. In the following example we try to update a TODO not named ‘Charlie’. Because the name is wrong, the .update function will raise a NotFoundError.
+
+
try: todos.update(Todo(id=1, title='Finish MiniDataAPI Spec', status='closed', name='Braden'))
+except NotFoundError as e: print('Record not updated')
+
+
Record not updated
+
+
+
Unlike poor Braden, Charlie isn’t filtered out. Let’s update his TODO.
+
+
todos.update(Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie'))
+
+
Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')
+
+
+
Finally, once constrained by .xtra, only records with Charlie as the name can be deleted.
+
+
try: todos.delete(1)
+except NotFoundError as e: print('Record not updated')
+
+
Record not updated
+
+
+
Charlie’s TODO was to finish development of FastHTML. While the framework will stabilize, like any good project it will see new features added and the odd bug corrected for many years to come. Therefore, Charlie’s TODO is nonsensical. Let’s delete it.
+
+
todos.delete(ct.id)
+
+
<Table todo (id, title, detail, status, name)>
+
+
+
When a TODO is inserted, the xtra fields are automatically set. This ensures that we don’t accidentally, for instance, insert items for others users. Note that here we don’t set the name field, but it’s still included in the resultant row:
+
+
ct = todos.insert(Todo(title='Rewrite personal site in FastHTML', status='open'))
+ct
+
+
Todo(id=3, title='Rewrite personal site in FastHTML', detail=None, status='open', name='Charlie')
+
+
+
If we try to change the username to someone else, the change is ignored, due to xtra:
+
+
ct.name ='Braden'
+todos.update(ct)
+
+
Todo(id=3, title='Rewrite personal site in FastHTML', detail=None, status='open', name='Charlie')
+
+
+
+
+
+
SQL-first design
+
+
users =None
+User =None
+
+
+
users = db.t.user
+users
+
+
<Table user (name, email, year_started, pwd)>
+
+
+
(This section needs to be documented properly.)
+
From the table objects we can extract a Dataclass version of our tables. Usually this is given an singular uppercase version of our table name, which in this case is User.
For creating new implementations, the code examples in this specification are the test case for the API. New implementations should pass the tests in order to be compliant with the specification.
+
+
+
Implementations
+
+
fastlite - The original implementation, only for Sqlite
+
fastsql - An SQL database agnostic implementation based on the excellent SQLAlchemy library.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/explains/minidataapi.html.md b/explains/minidataapi.html.md
new file mode 100644
index 00000000..7d803670
--- /dev/null
+++ b/explains/minidataapi.html.md
@@ -0,0 +1,666 @@
+# MiniDataAPI Spec
+
+
+
+
+The `MiniDataAPI` is a persistence API specification that designed to be
+small and relatively easy to implement across a wide range of
+datastores. While early implementations have been SQL-based, the
+specification can be quickly implemented in key/value stores, document
+databases, and more.
+
+
+
+> **Work in Progress**
+>
+> The MiniData API spec is a work in progress, subject to change. While
+> the majority of design is complete, expect there could be breaking
+> changes.
+
+
+
+## Why?
+
+The MiniDataAPI specification allows us to use the same API for many
+different database engines. Any application using the MiniDataAPI spec
+for interacting with its database requires no modification beyond import
+and configuration changes to switch database engines. For example, to
+convert an application from Fastlite running SQLite to FastSQL running
+PostgreSQL, should require only changing these two lines:
+
+
+
+As both libraries adhere to the MiniDataAPI specification, the rest of
+the code in the application should remain the same. The advantage of the
+MiniDataAPI spec is that it allows people to use whatever datastores
+they have access to or prefer.
+
+
+
+> **Note**
+>
+> Switching databases won’t migrate any existing data between databases.
+
+
+
+### Easy to learn, quick to implement
+
+The MiniDataAPI specification is designed to be easy-to-learn and quick
+to implement. It focuses on straightforward Create, Read, Update, and
+Delete (CRUD) operations.
+
+MiniDataAPI databases aren’t limited to just row-based systems. In fact,
+the specification is closer in design to a key/value store than a set of
+records. What’s exciting about this is we can write implementations for
+tools like Python dict stored as JSON, Redis, and even the venerable
+ZODB.
+
+### Limitations of the MiniDataAPI Specification
+
+> “Mini refers to the lightweightness of specification, not the data.”
+>
+> – Jeremy Howard
+
+The advantages of the MiniDataAPI come at a cost. The MiniDataAPI
+specification focuses a very small set of features compared to what can
+be found in full-fledged ORMs and query languages. It intentionally
+avoids nuances or sophisticated features.
+
+This means the specification does not include joins or formal foreign
+keys. Complex data stored over multiple tables that require joins isn’t
+handled well. For this kind of scenario it’s probably for the best to
+use more sophisticated ORMs or even direct database queries.
+
+### Summary of the MiniDataAPI Design
+
+- Easy-to-learn
+- Relative quick to implement for new database engines
+- An API for CRUD operations
+- For many different types of databases including row- and
+ key/value-based designs
+- Intentionally small in terms of features: no joins, no foreign keys,
+ no database specific features
+- Best for simpler designs, complex architectures will need more
+ sophisticated tools.
+
+## Connect/construct the database
+
+We connect or construct the database by passing in a string connecting
+to the database endpoint or a filepath representing the database’s
+location. While this example is for SQLite running in memory, other
+databases such as PostgreSQL, Redis, MongoDB, might instead use a URI
+pointing at the database’s filepath or endpoint. The method of
+connecting to a DB is *not* part of this API, but part of the underlying
+library. For instance, for fastlite:
+
+``` python
+db = database(':memory:')
+```
+
+Here’s a complete list of the available methods in the API, all
+documented below (assuming `db` is a database and `t` is a table):
+
+- `db.create`
+- `t.insert`
+- `t.delete`
+- `t.update`
+- `t[key]`
+- `t(...)`
+- `t.xtra`
+
+## Tables
+
+For the sake of expediency, this document uses a SQL example. However,
+tables can represent anything, not just the fundamental construct of a
+SQL databases. They might represent keys within a key/value structure or
+files on a hard-drive.
+
+### Creating tables
+
+We use a `create()` method attached to `Database` object (`db` in our
+example) to create the tables.
+
+``` python
+class User: name:str; email: str; year_started:int
+users = db.create(User, pk='name')
+users
+```
+
+
+
+If no `pk` is provided, `id` is assumed to be the primary key.
+Regardless of whether you mark a class as a dataclass or not, it will be
+turned into one – specifically into a
+[`flexiclass`](https://fastcore.fast.ai/xtras.html#flexiclass).
+
+``` python
+@dataclass
+class Todo: id: int; title: str; detail: str; status: str; name: str
+todos = db.create(Todo)
+todos
+```
+
+
+
+### Compound primary keys
+
+The MiniData API spec supports compound primary keys, where more than
+one column is used to identify records. We’ll also use this example to
+demonstrate creating a table using a dict of keyword arguments.
+
+``` python
+class Publication: authors: str; year: int; title: str
+publications = db.create(Publication, pk=('authors', 'year'))
+```
+
+### Transforming tables
+
+Depending on the database type, this method can include transforms - the
+ability to modify the tables. Let’s go ahead and add a password field
+for our table called `pwd`.
+
+``` python
+class User: name:str; email: str; year_started:int; pwd:str
+users = db.create(User, pk='name', transform=True)
+users
+```
+
+
+
+## Manipulating data
+
+The specification is designed to provide as straightforward CRUD API
+(Create, Read, Update, and Delete) as possible. Additional features like
+joins are out of scope.
+
+### .insert()
+
+Add a new record to the database. We want to support as many types as
+possible, for now we have tests for Python classes, dataclasses, and
+dicts. Returns an instance of the new record.
+
+Here’s how to add a record using a Python class:
+
+``` python
+users.insert(User(name='Braden', email='b@example.com', year_started=2018))
+```
+
+ User(name='Braden', email='b@example.com', year_started=2018, pwd=None)
+
+We can also use keyword arguments directly:
+
+``` python
+users.insert(name='Alma', email='a@example.com', year_started=2019)
+```
+
+ User(name='Alma', email='a@example.com', year_started=2019, pwd=None)
+
+And now Charlie gets added via a Python dict.
+
+``` python
+users.insert({'name': 'Charlie', 'email': 'c@example.com', 'year_started': 2018})
+```
+
+ User(name='Charlie', email='c@example.com', year_started=2018, pwd=None)
+
+And now TODOs. Note that the inserted row is returned:
+
+``` python
+todos.insert(Todo(title='Write MiniDataAPI spec', status='open', name='Braden'))
+todos.insert(title='Implement SSE in FastHTML', status='open', name='Alma')
+todo = todos.insert(dict(title='Finish development of FastHTML', status='closed', name='Charlie'))
+todo
+```
+
+ Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')
+
+Let’s do the same with the `Publications` table.
+
+``` python
+publications.insert(Publication(authors='Alma', year=2019, title='FastHTML'))
+publications.insert(authors='Alma', year=2030, title='FastHTML and beyond')
+publication= publications.insert((dict(authors='Alma', year=2035, title='FastHTML, the early years')))
+publication
+```
+
+ Publication(authors='Alma', year=2035, title='FastHTML, the early years')
+
+### Square bracket search \[\]
+
+Get a single record by entering a primary key into a table object within
+square brackets. Let’s see if we can find Alma.
+
+``` python
+user = users['Alma']
+user
+```
+
+ User(name='Alma', email='a@example.com', year_started=2019, pwd=None)
+
+If no record is found, a `NotFoundError` error is raised. Here we look
+for David, who hasn’t yet been added to our users table.
+
+``` python
+try: users['David']
+except NotFoundError: print(f'User not found')
+```
+
+ User not found
+
+Here’s a demonstration of a ticket search, demonstrating how this works
+with non-string primary keys.
+
+``` python
+todos[1]
+```
+
+ Todo(id=1, title='Write MiniDataAPI spec', detail=None, status='open', name='Braden')
+
+Compound primary keys can be supplied in lists or tuples, in the order
+they were defined. In this case it is the `authors` and `year` columns.
+
+Here’s a query by compound primary key done with a `list`:
+
+``` python
+publications[['Alma', 2019]]
+```
+
+ Publication(authors='Alma', year=2019, title='FastHTML')
+
+Here’s the same query done directly with index args.
+
+``` python
+publications['Alma', 2030]
+```
+
+ Publication(authors='Alma', year=2030, title='FastHTML and beyond')
+
+### Parentheses search ()
+
+Get zero to many records by entering values with parentheses searches.
+If nothing is in the parentheses, then everything is returned.
+
+``` python
+users()
+```
+
+ [User(name='Braden', email='b@example.com', year_started=2018, pwd=None),
+ User(name='Alma', email='a@example.com', year_started=2019, pwd=None),
+ User(name='Charlie', email='c@example.com', year_started=2018, pwd=None)]
+
+We can order the results.
+
+``` python
+users(order_by='name')
+```
+
+ [User(name='Alma', email='a@example.com', year_started=2019, pwd=None),
+ User(name='Braden', email='b@example.com', year_started=2018, pwd=None),
+ User(name='Charlie', email='c@example.com', year_started=2018, pwd=None)]
+
+We can filter on the results:
+
+``` python
+users(where="name='Alma'")
+```
+
+ [User(name='Alma', email='a@example.com', year_started=2019, pwd=None)]
+
+Generally you probably want to use placeholders, to avoid SQL injection
+attacks:
+
+``` python
+users("name=?", ('Alma',))
+```
+
+ [User(name='Alma', email='a@example.com', year_started=2019, pwd=None)]
+
+We can limit results with the `limit` keyword:
+
+``` python
+users(limit=1)
+```
+
+ [User(name='Braden', email='b@example.com', year_started=2018, pwd=None)]
+
+If we’re using the `limit` keyword, we can also use the `offset` keyword
+to start the query later.
+
+``` python
+users(limit=5, offset=1)
+```
+
+ [User(name='Alma', email='a@example.com', year_started=2019, pwd=None),
+ User(name='Charlie', email='c@example.com', year_started=2018, pwd=None)]
+
+### .update()
+
+Update an existing record of the database. Must accept Python dict,
+dataclasses, and standard classes. Uses the primary key for identifying
+the record to be changed. Returns an instance of the updated record.
+
+Here’s with a normal Python class:
+
+``` python
+user
+```
+
+ User(name='Alma', email='a@example.com', year_started=2019, pwd=None)
+
+``` python
+user.year_started = 2099
+users.update(user)
+```
+
+ User(name='Alma', email='a@example.com', year_started=2099, pwd=None)
+
+Or use a dict:
+
+``` python
+users.update(dict(name='Alma', year_started=2199, email='a@example.com'))
+```
+
+ User(name='Alma', email='a@example.com', year_started=2199, pwd=None)
+
+Or use kwargs:
+
+``` python
+users.update(name='Alma', year_started=2149)
+```
+
+ User(name='Alma', email='a@example.com', year_started=2149, pwd=None)
+
+If the primary key doesn’t match a record, raise a `NotFoundError`.
+
+John hasn’t started with us yet so doesn’t get the chance yet to travel
+in time.
+
+``` python
+try: users.update(User(name='John', year_started=2024, email='j@example.com'))
+except NotFoundError: print('User not found')
+```
+
+ User not found
+
+### .delete()
+
+Delete a record of the database. Uses the primary key for identifying
+the record to be removed. Returns a table object.
+
+Charlie decides to not travel in time. He exits our little group.
+
+``` python
+users.delete('Charlie')
+```
+
+
+
+If the primary key value can’t be found, raises a `NotFoundError`.
+
+``` python
+try: users.delete('Charlies')
+except NotFoundError: print('User not found')
+```
+
+ User not found
+
+In John’s case, he isn’t time travelling with us yet so can’t be
+removed.
+
+``` python
+try: users.delete('John')
+except NotFoundError: print('User not found')
+```
+
+ User not found
+
+Deleting records with compound primary keys requires providing the
+entire key.
+
+``` python
+publications.delete(['Alma' , 2035])
+```
+
+
+
+### `in` keyword
+
+Are `Alma` and `John` contained `in` the Users table? Or, to be
+technically precise, is the item with the specified primary key value
+`in` this table?
+
+``` python
+'Alma' in users, 'John' in users
+```
+
+ (True, False)
+
+Also works with compound primary keys, as shown below. You’ll note that
+the operation can be done with either a `list` or `tuple`.
+
+``` python
+['Alma', 2019] in publications
+```
+
+ True
+
+And now for a `False` result, where John has no publications.
+
+``` python
+('John', 1967) in publications
+```
+
+ False
+
+### .xtra()
+
+If we set fields within the `.xtra` function to a particular value, then
+indexing is also filtered by those. This applies to every database
+method except for record creation. This makes it easier to limit users
+(or other objects) access to only things for which they have permission.
+
+For example, if we query all our records below without setting values v
+ia the `.xtra` function, we can see todos for everyone. Pay special
+attention to the `id` values of all three records, as we are about to
+filter most of them away.
+
+``` python
+todos()
+```
+
+ [Todo(id=1, title='Write MiniDataAPI spec', detail=None, status='open', name='Braden'),
+ Todo(id=2, title='Implement SSE in FastHTML', detail=None, status='open', name='Alma'),
+ Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')]
+
+Let’s use `.xtra` to constrain results just to Charlie. We set the
+`name` field in Todos, but it could be any field defined for this table.
+
+``` python
+todos.xtra(name='Charlie')
+```
+
+We’ve now set a field to a value with `.xtra`, if we loop over all the
+records again, only those assigned to records with a `name` of `Charlie`
+will be displayed.
+
+``` python
+todos()
+```
+
+ [Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')]
+
+The `in` keyword is also affected. Only records with a `name` of Charlie
+will evaluate to be `True`. Let’s demonstrate by testing it with a
+Charlie record:
+
+``` python
+ct = todos[3]
+ct
+```
+
+ Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')
+
+Charlie’s record has an ID of 3. Here we demonstrate that Charlie’s TODO
+can be found in the list of todos:
+
+``` python
+ct.id in todos
+```
+
+ True
+
+If we try `in` with the other IDs the query fails because the filtering
+is now set to just records with a name of Charlie.
+
+``` python
+1 in todos, 2 in todos
+```
+
+ (False, False)
+
+``` python
+try: todos[2]
+except NotFoundError: print('Record not found')
+```
+
+ Record not found
+
+We are also constrained by what records we can update. In the following
+example we try to update a TODO not named ‘Charlie’. Because the name is
+wrong, the `.update` function will raise a `NotFoundError`.
+
+``` python
+try: todos.update(Todo(id=1, title='Finish MiniDataAPI Spec', status='closed', name='Braden'))
+except NotFoundError as e: print('Record not updated')
+```
+
+ Record not updated
+
+Unlike poor Braden, Charlie isn’t filtered out. Let’s update his TODO.
+
+``` python
+todos.update(Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie'))
+```
+
+ Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')
+
+Finally, once constrained by `.xtra`, only records with Charlie as the
+name can be deleted.
+
+``` python
+try: todos.delete(1)
+except NotFoundError as e: print('Record not updated')
+```
+
+ Record not updated
+
+Charlie’s TODO was to finish development of FastHTML. While the
+framework will stabilize, like any good project it will see new features
+added and the odd bug corrected for many years to come. Therefore,
+Charlie’s TODO is nonsensical. Let’s delete it.
+
+``` python
+todos.delete(ct.id)
+```
+
+
+
+When a TODO is inserted, the `xtra` fields are automatically set. This
+ensures that we don’t accidentally, for instance, insert items for
+others users. Note that here we don’t set the `name` field, but it’s
+still included in the resultant row:
+
+``` python
+ct = todos.insert(Todo(title='Rewrite personal site in FastHTML', status='open'))
+ct
+```
+
+ Todo(id=3, title='Rewrite personal site in FastHTML', detail=None, status='open', name='Charlie')
+
+If we try to change the username to someone else, the change is ignored,
+due to `xtra`:
+
+``` python
+ct.name = 'Braden'
+todos.update(ct)
+```
+
+ Todo(id=3, title='Rewrite personal site in FastHTML', detail=None, status='open', name='Charlie')
+
+## SQL-first design
+
+``` python
+users = None
+User = None
+```
+
+``` python
+users = db.t.user
+users
+```
+
+
+
+(This section needs to be documented properly.)
+
+From the table objects we can extract a Dataclass version of our tables.
+Usually this is given an singular uppercase version of our table name,
+which in this case is `User`.
+
+``` python
+User = users.dataclass()
+```
+
+``` python
+User(name='Braden', email='b@example.com', year_started=2018)
+```
+
+ User(name='Braden', email='b@example.com', year_started=2018, pwd=UNSET)
+
+## Implementations
+
+### Implementing MiniDataAPI for a new datastore
+
+For creating new implementations, the code examples in this
+specification are the test case for the API. New implementations should
+pass the tests in order to be compliant with the specification.
+
+### Implementations
+
+- [fastlite](https://github.com/AnswerDotAI/fastlite) - The original
+ implementation, only for Sqlite
+- [fastsql](https://github.com/AnswerDotAI/fastsql) - An SQL database
+ agnostic implementation based on the excellent SQLAlchemy library.
diff --git a/explains/oauth.html b/explains/oauth.html
new file mode 100644
index 00000000..35623bdc
--- /dev/null
+++ b/explains/oauth.html
@@ -0,0 +1,1031 @@
+
+
+
+
+
+
+
+
+
+OAuth – fasthtml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
OAuth is an open standard for ‘access delegation’, commonly used as a way for Internet users to grant websites or applications access to their information on other websites but without giving them the passwords. It is the mechanism that enables “Log in with Google” on many sites, saving you from having to remember and manage yet another password. Like many auth-related topics, there’s a lot of depth and complexity to the OAuth standard, but once you understand the basic usage it can be a very convenient alternative to managing your own user accounts.
+
On this page you’ll see how to use OAuth with FastHTML to implement some common pieces of functionality.
+
In FastHTML you set up a client like GoogleAppClient. The client is responsible for storing the client ID and client secret, and for handling the OAuth flow. Let’s run through three examples, illustrating some important concepts across three different OAuth providers.
+
+
A Minimal Login Flow (GitHub)
+
Let’s begin by building a minimal ‘Sign in with GitHub’ flow. This will demonstrate the basic steps of OAuth.
+
OAuth requires a “provider” (in this case, GitHub) to authenticate the user. So the first step when setting up our app is to register with GitHub to set things up.
+
Go to https://github.com/settings/developers and click “New OAuth App”. Fill in the form with the following values, then click ‘Register application’.
+
+
Application name: Your app name
+
Homepage URL: http://localhost:8000 (or whatever URL you’re using - you can change this later)
+
Authorization callback URL: http://localhost:8000/auth_redirect (you can modify this later too)
+
+
+
+
+
After you register, you’ll see a screen where you can view the client ID and generate a client secret. Store these values in a safe place. You’ll use them to create a GitHubAppClient object in FastHTML.
+
This client object is responsible for handling the parts of the OAuth flow which depend on direct communication between your app and GitHub, as opposed to interactions which go through the user’s browser via redirects.
You should also save the path component of the authorization callback URL which you provided on registration.
+
This route is where GitHub will redirect the user’s browser in order to send an authorization code to your app. You should save only the URL’s path component rather than the entire URL because you want your code to work automatically in deployment, when the host and port part of the URL change from localhost:8000 to your real DNS name.
+
Save the special authorization callback path under an obvious name:
+
auth_callback_path ="/auth_redirect"
+
+
+
+
+
+
+Note
+
+
+
+
It’s recommended to store the client ID, and secret, in environment variables, rather than hardcoding them in your code.
+
+
+
When the user visit a normal page of your app, if they are not already logged in, then you’ll want to redirect them to your app’s login page, which will live at the /login path. We accomplish that by using this piece of “beforeware”, which defines logic which runs before other work for all routes except ones we specify to be skipped:
We configure the beforeware to skip /login because that’s where the user goes to login, and we also skip the special authorization callback path because that is used by OAuth itself to receive information from GitHub.
+
It’s only at your login page that we start the OAuth flow. To start the OAuth flow, you need to give the user a link to GitHub’s login for your app. You’ll need the client object to generate that link, and the client object will in turn need the full authorization callback URL, which we need to build from the authorization callback path, so it is a multi-step process to produce this GitHub login link.
+
Here is an implementation of your own /login route handler. It generates the GitHub login link and presents it to the user:
Once the user follows that link, GitHub will ask them to grant permission to your app to access their GitHub account. If they agree, GitHub will redirect them back to your app’s authorization callback URL, carrying an authorization code which your app can use to generate an access token. To receive this code, you need to set up a route in FastHTML that listens for requests at the authorization callback path. For example:
This authorization code is temporary, and is used by your app to directly ask the provider for user information like an access token.
+
To recap, you can think of the exchange so far as:
+
+
User to us: “I want to log in with you, app.”
+
Us to User: “Okay but first, here’s a special link to log in with GitHub”
+
User to GitHub: “I want to log in with you, GitHub, to use this app.”
+
GitHub to User: “OK, redirecting you back to the app’s URL (with an auth code)”
+
User to Us: “Hi again, app. Here’s the GitHub auth code you need to ask GitHub for info about me” (delivered via /auth_redirect?code=...)
+
+
The final steps we need to implement are as follows:
+
+
Us to GitHUb: “A user just gave me this auth code. May I have the user info (e.g., an access token)?”
+
GitHub to us: “Since you have an auth code, here’s the user info”
+
+
It’s critical for us to derive the user info from the auth code immediately in the authorization callback, because the auth code may be used only once. So we use it that once in order to get information like an access token, which will remain valid for longer.
+
To go from the auth code to user info, you use info = client.retr_info(code,redirect_uri). From the user info, you can extract the user_id, which is a unique identifier for the user:
But we want the user ID not to print it but to remember the user.
+
So let us store it in the session object, to remember who is logged in:
+
@app.get(auth_callback_path)
+def auth_redirect(code:str, request, session):
+ redir = redir_url(request, auth_callback_path)
+ user_info = client.retr_info(code, redir)
+ user_id = user_info[client.id_key] # get their ID
+ session['user_id'] = user_id # save ID in the session
+return RedirectResponse('/', status_code=303)
+
The session object is derived from values visible to the user’s browser, but it is cryptographically signed so the user can’t read it themselves. This makes it safe to store even information we don’t want to expose to the user.
+
For larger quantities of data, we’d want to save that information in a database and use the session to hold keys to lookup information from that database.
+
Here’s a minimal app that puts all these pieces together. It uses the user info to get the user_id. It stores that in the session object. It then uses the user_id as a key into a database, which tracks how frequently every user has hit an increment button.
+
import os
+from fasthtml.common import*
+from fasthtml.oauth import GitHubAppClient, redir_url
+
+db = database('data/counts.db')
+counts = db.t.counts
+if counts notin db.t: counts.create(dict(name=str, count=int), pk='name')
+Count = counts.dataclass()
+
+# Auth client setup for GitHub
+client = GitHubAppClient(os.getenv("AUTH_CLIENT_ID"),
+ os.getenv("AUTH_CLIENT_SECRET"))
+auth_callback_path ="/auth_redirect"
+
+def before(req, session):
+# if not logged in, we send them to our login page
+# logged in means:
+# - 'user_id' in the session object,
+# - 'auth' in the request object
+ auth = req.scope['auth'] = session.get('user_id', None)
+ifnot auth: return RedirectResponse('/login', status_code=303)
+ counts.xtra(name=auth)
+bware = Beforeware(before, skip=['/login', auth_callback_path])
+
+app = FastHTML(before=bware)
+
+# User asks us to Login
+@app.get('/login')
+def login(request):
+ redir = redir_url(request,auth_callback_path)
+ login_link = client.login_link(redir)
+# we tell user to login at github
+return P(A('Login with GitHub', href=login_link))
+
+# User comes back to us with an auth code from Github
+@app.get(auth_callback_path)
+def auth_redirect(code:str, request, session):
+ redir = redir_url(request, auth_callback_path)
+ user_info = client.retr_info(code, redir)
+ user_id = user_info[client.id_key] # get their ID
+ session['user_id'] = user_id # save ID in the session
+# create a db entry for the user
+if user_id notin counts: counts.insert(name=user_id, count=0)
+return RedirectResponse('/', status_code=303)
+
+@app.get('/')
+def home(auth):
+return Div(
+ P("Count demo"),
+ P(f"Count: ", Span(counts[auth].count, id='count')),
+ Button('Increment', hx_get='/increment', hx_target='#count'),
+ P(A('Logout', href='/logout'))
+ )
+
+@app.get('/increment')
+def increment(auth):
+ c = counts[auth]
+ c.count +=1
+return counts.upsert(c).count
+
+@app.get('/logout')
+def logout(session):
+ session.pop('user_id', None)
+return RedirectResponse('/login', status_code=303)
+
+serve()
+
Some things to note:
+
+
The before function is used to check if the user is authenticated. If not, they are redirected to the login page.
+
To log the user out, we remove the user ID from the session.
+
Calling counts.xtra(name=auth) ensures that only the row corresponding to the current user is accessible when responding to a request. This is often nicer than trying to remember to filter the data in every route, and lowers the risk of accidentally leaking data.
+
In the auth_redirect route, we store the user ID in the session and create a new row in the user_counts table if it doesn’t already exist.
+
+
You can find more heavily-commented version of this code in the oauth directory in fasthtml-example, along with an even more minimal example. More examples may be added in the future.
+
+
Revoking Tokens (Google)
+
When the user in the example above logs out, we remove their user ID from the session. However, the user is still logged in to GitHub. If they click ‘Login with GitHub’ again, they’ll be redirected back to our site without having to log in again. This is because GitHub remembers that they’ve already granted our app permission to access their account. Most of the time this is convenient, but for testing or security purposes you may want a way to revoke this permission.
+
As a user, you can usually revoke access to an app from the provider’s website (for example, https://github.com/settings/applications). But as a developer, you can also revoke access programmatically - at least with some providers. This requires keeping track of the access token (stored in client.token["access_token"] after you call retr_info), and sending a request to the provider’s revoke URL:
Not all providers support token revocation, and it is not built into FastHTML clients at the moment.
+
+
+
Using State (Hugging Face)
+
Imagine a user (not logged in) comes to your AI image editing site, starts testing things out, and then realizes they need to sign in before they can click “Run (Pro)” on the edit they’re working on. They click “Sign in with Hugging Face”, log in, and are redirected back to your site. But now they’ve lost their in-progress edit and are left just looking at the homepage! This is an example of a case where you might want to keep track of some additional state. Another strong use case for being able to pass some uniqie state through the OAuth flow is to prevent something called a CSRF attack. To add a state string to the OAuth flow, you can use client.login_link_with_state(state) instead of client.login_link(), like so:
+
# in login page:
+link = A('Login with GitHub', href=client.login_link_with_state(state='current_prompt: add a unicorn'))
+
+# in auth_redirect:
+@app.get('/auth_redirect')
+def auth_redirect(code:str, session, state:str=None):
+print(f"state: {state}") # Use as needed
+ ...
+
The state string is passed through the OAuth flow and back to your site.
+
+
+
A Work in Progress
+
This page (and OAuth support in FastHTML) is a work in progress. Questions, PRs, and feedback are welcome!
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/explains/oauth.html.md b/explains/oauth.html.md
new file mode 100644
index 00000000..6c7b8228
--- /dev/null
+++ b/explains/oauth.html.md
@@ -0,0 +1,360 @@
+# OAuth
+
+
+
+
+OAuth is an open standard for ‘access delegation’, commonly used as a
+way for Internet users to grant websites or applications access to their
+information on other websites but without giving them the passwords. It
+is the mechanism that enables “Log in with Google” on many sites, saving
+you from having to remember and manage yet another password. Like many
+auth-related topics, there’s a lot of depth and complexity to the OAuth
+standard, but once you understand the basic usage it can be a very
+convenient alternative to managing your own user accounts.
+
+On this page you’ll see how to use OAuth with FastHTML to implement some
+common pieces of functionality.
+
+In FastHTML you set up a client like
+[`GoogleAppClient`](https://AnswerDotAI.github.io/fasthtml/api/oauth.html#googleappclient).
+The client is responsible for storing the client ID and client secret,
+and for handling the OAuth flow. Let’s run through three examples,
+illustrating some important concepts across three different OAuth
+providers.
+
+## A Minimal Login Flow (GitHub)
+
+Let’s begin by building a minimal ‘Sign in with GitHub’ flow. This will
+demonstrate the basic steps of OAuth.
+
+OAuth requires a “provider” (in this case, GitHub) to authenticate the
+user. So the first step when setting up our app is to register with
+GitHub to set things up.
+
+Go to https://github.com/settings/developers and click “New OAuth App”.
+Fill in the form with the following values, then click ‘Register
+application’.
+
+- Application name: Your app name
+- Homepage URL: http://localhost:8000 (or whatever URL you’re using -
+ you can change this later)
+- Authorization callback URL: http://localhost:8000/auth_redirect (you
+ can modify this later too)
+
+
+
+
+
+
+
+After you register, you’ll see a screen where you can view the client ID
+and generate a client secret. Store these values in a safe place. You’ll
+use them to create a
+[`GitHubAppClient`](https://AnswerDotAI.github.io/fasthtml/api/oauth.html#githubappclient)
+object in FastHTML.
+
+This `client` object is responsible for handling the parts of the OAuth
+flow which depend on direct communication between your app and GitHub,
+as opposed to interactions which go through the user’s browser via
+redirects.
+
+Here is how to setup the client object:
+
+``` python
+client = GitHubAppClient(
+ client_id="your_client_id",
+ client_secret="your_client_secret"
+)
+```
+
+You should also save the path component of the authorization callback
+URL which you provided on registration.
+
+This route is where GitHub will redirect the user’s browser in order to
+send an authorization code to your app. You should save only the URL’s
+path component rather than the entire URL because you want your code to
+work automatically in deployment, when the host and port part of the URL
+change from `localhost:8000` to your real DNS name.
+
+Save the special authorization callback path under an obvious name:
+
+``` python
+auth_callback_path = "/auth_redirect"
+```
+
+
+
+> **Note**
+>
+> It’s recommended to store the client ID, and secret, in environment
+> variables, rather than hardcoding them in your code.
+
+
+
+When the user visit a normal page of your app, if they are not already
+logged in, then you’ll want to redirect them to your app’s login page,
+which will live at the `/login` path. We accomplish that by using this
+piece of “beforeware”, which defines logic which runs before other work
+for all routes except ones we specify to be skipped:
+
+``` python
+def before(req, session):
+ auth = req.scope['auth'] = session.get('user_id', None)
+ if not auth: return RedirectResponse('/login', status_code=303)
+ counts.xtra(name=auth)
+bware = Beforeware(before, skip=['/login', auth_callback_path])
+```
+
+We configure the beforeware to skip `/login` because that’s where the
+user goes to login, and we also skip the special authorization callback
+path because that is used by OAuth itself to receive information from
+GitHub.
+
+It’s only at your login page that we start the OAuth flow. To start the
+OAuth flow, you need to give the user a link to GitHub’s login for your
+app. You’ll need the `client` object to generate that link, and the
+client object will in turn need the full authorization callback URL,
+which we need to build from the authorization callback path, so it is a
+multi-step process to produce this GitHub login link.
+
+Here is an implementation of your own `/login` route handler. It
+generates the GitHub login link and presents it to the user:
+
+``` python
+@app.get('/login')
+def login(request)
+ redir = redir_url(request,auth_callback_path)
+ login_link = client.login_link(redir)
+ return P(A('Login with GitHub', href=login_link))
+```
+
+Once the user follows that link, GitHub will ask them to grant
+permission to your app to access their GitHub account. If they agree,
+GitHub will redirect them back to your app’s authorization callback URL,
+carrying an authorization code which your app can use to generate an
+access token. To receive this code, you need to set up a route in
+FastHTML that listens for requests at the authorization callback path.
+For example:
+
+``` python
+@app.get(auth_callback_path)
+def auth_redirect(code:str):
+ return P(f"code: {code}")
+```
+
+This authorization code is temporary, and is used by your app to
+directly ask the provider for user information like an access token.
+
+To recap, you can think of the exchange so far as:
+
+- User to us: “I want to log in with you, app.”
+- Us to User: “Okay but first, here’s a special link to log in with
+ GitHub”
+- User to GitHub: “I want to log in with you, GitHub, to use this app.”
+- GitHub to User: “OK, redirecting you back to the app’s URL (with an
+ auth code)”
+- User to Us: “Hi again, app. Here’s the GitHub auth code you need to
+ ask GitHub for info about me” (delivered via
+ `/auth_redirect?code=...`)
+
+The final steps we need to implement are as follows:
+
+- Us to GitHUb: “A user just gave me this auth code. May I have the user
+ info (e.g., an access token)?”
+- GitHub to us: “Since you have an auth code, here’s the user info”
+
+It’s critical for us to derive the user info from the auth code
+immediately in the authorization callback, because the auth code may be
+used only once. So we use it that once in order to get information like
+an access token, which will remain valid for longer.
+
+To go from the auth code to user info, you use
+`info = client.retr_info(code,redirect_uri)`. From the user info, you
+can extract the `user_id`, which is a unique identifier for the user:
+
+``` python
+@app.get(auth_callback_path)
+def auth_redirect(code:str, request):
+ redir = redir_url(request, auth_callback_path)
+ user_info = client.retr_info(code, redir)
+ user_id = info[client.id_key]
+ return P(f"User id: {user_id}")
+```
+
+But we want the user ID not to print it but to remember the user.
+
+So let us store it in the `session` object, to remember who is logged
+in:
+
+``` python
+@app.get(auth_callback_path)
+def auth_redirect(code:str, request, session):
+ redir = redir_url(request, auth_callback_path)
+ user_info = client.retr_info(code, redir)
+ user_id = user_info[client.id_key] # get their ID
+ session['user_id'] = user_id # save ID in the session
+ return RedirectResponse('/', status_code=303)
+```
+
+The session object is derived from values visible to the user’s browser,
+but it is cryptographically signed so the user can’t read it themselves.
+This makes it safe to store even information we don’t want to expose to
+the user.
+
+For larger quantities of data, we’d want to save that information in a
+database and use the session to hold keys to lookup information from
+that database.
+
+Here’s a minimal app that puts all these pieces together. It uses the
+user info to get the user_id. It stores that in the session object. It
+then uses the user_id as a key into a database, which tracks how
+frequently every user has hit an increment button.
+
+``` python
+import os
+from fasthtml.common import *
+from fasthtml.oauth import GitHubAppClient, redir_url
+
+db = database('data/counts.db')
+counts = db.t.counts
+if counts not in db.t: counts.create(dict(name=str, count=int), pk='name')
+Count = counts.dataclass()
+
+# Auth client setup for GitHub
+client = GitHubAppClient(os.getenv("AUTH_CLIENT_ID"),
+ os.getenv("AUTH_CLIENT_SECRET"))
+auth_callback_path = "/auth_redirect"
+
+def before(req, session):
+ # if not logged in, we send them to our login page
+ # logged in means:
+ # - 'user_id' in the session object,
+ # - 'auth' in the request object
+ auth = req.scope['auth'] = session.get('user_id', None)
+ if not auth: return RedirectResponse('/login', status_code=303)
+ counts.xtra(name=auth)
+bware = Beforeware(before, skip=['/login', auth_callback_path])
+
+app = FastHTML(before=bware)
+
+# User asks us to Login
+@app.get('/login')
+def login(request):
+ redir = redir_url(request,auth_callback_path)
+ login_link = client.login_link(redir)
+ # we tell user to login at github
+ return P(A('Login with GitHub', href=login_link))
+
+# User comes back to us with an auth code from Github
+@app.get(auth_callback_path)
+def auth_redirect(code:str, request, session):
+ redir = redir_url(request, auth_callback_path)
+ user_info = client.retr_info(code, redir)
+ user_id = user_info[client.id_key] # get their ID
+ session['user_id'] = user_id # save ID in the session
+ # create a db entry for the user
+ if user_id not in counts: counts.insert(name=user_id, count=0)
+ return RedirectResponse('/', status_code=303)
+
+@app.get('/')
+def home(auth):
+ return Div(
+ P("Count demo"),
+ P(f"Count: ", Span(counts[auth].count, id='count')),
+ Button('Increment', hx_get='/increment', hx_target='#count'),
+ P(A('Logout', href='/logout'))
+ )
+
+@app.get('/increment')
+def increment(auth):
+ c = counts[auth]
+ c.count += 1
+ return counts.upsert(c).count
+
+@app.get('/logout')
+def logout(session):
+ session.pop('user_id', None)
+ return RedirectResponse('/login', status_code=303)
+
+serve()
+```
+
+Some things to note:
+
+- The `before` function is used to check if the user is authenticated.
+ If not, they are redirected to the login page.
+- To log the user out, we remove the user ID from the session.
+- Calling `counts.xtra(name=auth)` ensures that only the row
+ corresponding to the current user is accessible when responding to a
+ request. This is often nicer than trying to remember to filter the
+ data in every route, and lowers the risk of accidentally leaking data.
+- In the `auth_redirect` route, we store the user ID in the session and
+ create a new row in the `user_counts` table if it doesn’t already
+ exist.
+
+You can find more heavily-commented version of this code in the [oauth
+directory in
+fasthtml-example](https://github.com/AnswerDotAI/fasthtml-example/tree/main/oauth_example),
+along with an even more minimal example. More examples may be added in
+the future.
+
+### Revoking Tokens (Google)
+
+When the user in the example above logs out, we remove their user ID
+from the session. However, the user is still logged in to GitHub. If
+they click ‘Login with GitHub’ again, they’ll be redirected back to our
+site without having to log in again. This is because GitHub remembers
+that they’ve already granted our app permission to access their account.
+Most of the time this is convenient, but for testing or security
+purposes you may want a way to revoke this permission.
+
+As a user, you can usually revoke access to an app from the provider’s
+website (for example, ). But
+as a developer, you can also revoke access programmatically - at least
+with some providers. This requires keeping track of the access token
+(stored in `client.token["access_token"]` after you call `retr_info`),
+and sending a request to the provider’s revoke URL:
+
+``` python
+auth_revoke_url = "https://accounts.google.com/o/oauth2/revoke"
+def revoke_token(token):
+ response = requests.post(auth_revoke_url, params={"token": token})
+ return response.status_code == 200 # True if successful
+```
+
+Not all providers support token revocation, and it is not built into
+FastHTML clients at the moment.
+
+### Using State (Hugging Face)
+
+Imagine a user (not logged in) comes to your AI image editing site,
+starts testing things out, and then realizes they need to sign in before
+they can click “Run (Pro)” on the edit they’re working on. They click
+“Sign in with Hugging Face”, log in, and are redirected back to your
+site. But now they’ve lost their in-progress edit and are left just
+looking at the homepage! This is an example of a case where you might
+want to keep track of some additional state. Another strong use case for
+being able to pass some uniqie state through the OAuth flow is to
+prevent something called a [CSRF
+attack](https://en.wikipedia.org/wiki/Cross-site_request_forgery). To
+add a state string to the OAuth flow, you can use
+`client.login_link_with_state(state)` instead of `client.login_link()`,
+like so:
+
+``` python
+# in login page:
+link = A('Login with GitHub', href=client.login_link_with_state(state='current_prompt: add a unicorn'))
+
+# in auth_redirect:
+@app.get('/auth_redirect')
+def auth_redirect(code:str, session, state:str=None):
+ print(f"state: {state}") # Use as needed
+ ...
+```
+
+The state string is passed through the OAuth flow and back to your site.
+
+### A Work in Progress
+
+This page (and OAuth support in FastHTML) is a work in progress.
+Questions, PRs, and feedback are welcome!
diff --git a/explains/routes.html b/explains/routes.html
new file mode 100644
index 00000000..518bcb22
--- /dev/null
+++ b/explains/routes.html
@@ -0,0 +1,1001 @@
+
+
+
+
+
+
+
+
+
+Routes – fasthtml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Behaviour in FastHTML apps is defined by routes. The syntax is largely the same as the wonderful FastAPI (which is what you should be using instead of this if you’re creating a JSON service. FastHTML is mainly for making HTML web apps, not APIs).
+
+
+
+
+
+
+Unfinished
+
+
+
+
We haven’t yet written complete documentation of all of FastHTML’s routing features – until we add that, the best place to see all the available functionality is to look over the tests
+
+
+
Note that you need to include the types of your parameters, so that FastHTML knows what to pass to your function. Here, we’re just expecting a string:
+
+
from fasthtml.common import*
+
+
+
app = FastHTML()
+
+@app.get('/user/{nm}')
+def get_nm(nm:str): returnf"Good day to you, {nm}!"
+
+
Normally you’d save this into a file such as main.py, and then run it in uvicorn using:
+
uvicorn main:app
+
However, for testing, we can use Starlette’s TestClient to try it out:
TestClient uses httpx behind the scenes, so it returns a httpx.Response, which has a text attribute with our response body:
+
+
r.text
+
+
'Good day to you, Jeremy!'
+
+
+
In the previous example, the function name (get_nm) didn’t actually matter – we could have just called it _, for instance, since we never actually call it directly. It’s just called through HTTP. In fact, we often do call our functions _ when using this style of route, since that’s one less thing we have to worry about, naming.
+
An alternative approach to creating a route is to use app.route instead, in which case, you make the function name the HTTP method you want. Since this is such a common pattern, you might like to give a shorter name to app.route – we normally use rt:
FastHTML supports custom decorators for adding specific functionality to routes. This allows you to implement authentication, authorization, middleware, or other custom behaviors for individual routes.
+
Here’s an example of a basic authentication decorator:
The decorator intercepts the request before the route function executes. If the decorator allows the request to proceed, it calls the original route function, passing along the request and any other arguments.
+
One of the key advantages of this approach is the ability to apply different behaviors to different routes. You can also stack multiple decorators on a single route for combined functionality.
This flexiblity allows for granular control over route behaviour, enabling you to tailor each endpoint’s functionality as needed. While app-level beforeware remains useful for global operations, decorators provide a powerful tool for route-specific customization.
+
+
+
Combining Routes
+
Sometimes a FastHTML project can grow so weildy that putting all the routes into main.py becomes unweildy. Or, we install a FastHTML- or Starlette-based package that requires us to add routes.
+
First let’s create a books.py module, that represents all the user-related views:
+
+
# books.py
+books_app, rt = fast_app()
+
+books = ['A Guide to FastHTML', 'FastHTML Cookbook', 'FastHTML in 24 Hours']
+
+@rt("/", name="list")
+def get():
+return Titled("Books", *[P(book) for book in books])
+We use starlette.Mount to add the route to our routes list. We provide the name of books to make discovery and management of the links easier. More on that in items 2 and 3 of this annotations list
+
+
2
+
+This example link to the books list view is hand-crafted. Obvious in purpose, it makes changing link patterns in the future harder
+
+
3
+
+This example link uses the named URL route for the books. The advantage of this approach is it makes management of large numbers of link items easier.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/explains/routes.html.md b/explains/routes.html.md
new file mode 100644
index 00000000..a5373067
--- /dev/null
+++ b/explains/routes.html.md
@@ -0,0 +1,217 @@
+# Routes
+
+
+
+
+Behaviour in FastHTML apps is defined by routes. The syntax is largely
+the same as the wonderful [FastAPI](https://fastapi.tiangolo.com/)
+(which is what you should be using instead of this if you’re creating a
+JSON service. FastHTML is mainly for making HTML web apps, not APIs).
+
+
+
+> **Unfinished**
+>
+> We haven’t yet written complete documentation of all of FastHTML’s
+> routing features – until we add that, the best place to see all the
+> available functionality is to look over [the
+> tests](../api/core.html#tests)
+
+
+
+Note that you need to include the types of your parameters, so that
+[`FastHTML`](https://AnswerDotAI.github.io/fasthtml/api/core.html#fasthtml)
+knows what to pass to your function. Here, we’re just expecting a
+string:
+
+``` python
+from fasthtml.common import *
+```
+
+``` python
+app = FastHTML()
+
+@app.get('/user/{nm}')
+def get_nm(nm:str): return f"Good day to you, {nm}!"
+```
+
+Normally you’d save this into a file such as main.py, and then run it in
+`uvicorn` using:
+
+ uvicorn main:app
+
+However, for testing, we can use Starlette’s `TestClient` to try it out:
+
+``` python
+from starlette.testclient import TestClient
+```
+
+``` python
+client = TestClient(app)
+r = client.get('/user/Jeremy')
+r
+```
+
+
+
+TestClient uses `httpx` behind the scenes, so it returns a
+`httpx.Response`, which has a `text` attribute with our response body:
+
+``` python
+r.text
+```
+
+ 'Good day to you, Jeremy!'
+
+In the previous example, the function name (`get_nm`) didn’t actually
+matter – we could have just called it `_`, for instance, since we never
+actually call it directly. It’s just called through HTTP. In fact, we
+often do call our functions `_` when using this style of route, since
+that’s one less thing we have to worry about, naming.
+
+An alternative approach to creating a route is to use `app.route`
+instead, in which case, you make the function name the HTTP method you
+want. Since this is such a common pattern, you might like to give a
+shorter name to `app.route` – we normally use `rt`:
+
+``` python
+rt = app.route
+
+@rt('/')
+def post(): return "Going postal!"
+
+client.post('/').text
+```
+
+ 'Going postal!'
+
+### Route-specific functionality
+
+FastHTML supports custom decorators for adding specific functionality to
+routes. This allows you to implement authentication, authorization,
+middleware, or other custom behaviors for individual routes.
+
+Here’s an example of a basic authentication decorator:
+
+``` python
+from functools import wraps
+
+def basic_auth(f):
+ @wraps(f)
+ async def wrapper(req, *args, **kwargs):
+ token = req.headers.get("Authorization")
+ if token == 'abc123':
+ return await f(req, *args, **kwargs)
+ return Response('Not Authorized', status_code=401)
+ return wrapper
+
+@app.get("/protected")
+@basic_auth
+async def protected(req):
+ return "Protected Content"
+
+client.get('/protected', headers={'Authorization': 'abc123'}).text
+```
+
+ 'Protected Content'
+
+The decorator intercepts the request before the route function executes.
+If the decorator allows the request to proceed, it calls the original
+route function, passing along the request and any other arguments.
+
+One of the key advantages of this approach is the ability to apply
+different behaviors to different routes. You can also stack multiple
+decorators on a single route for combined functionality.
+
+``` python
+def app_beforeware():
+ print('App level beforeware')
+
+app = FastHTML(before=Beforeware(app_beforeware))
+client = TestClient(app)
+
+def route_beforeware(f):
+ @wraps(f)
+ async def decorator(*args, **kwargs):
+ print('Route level beforeware')
+ return await f(*args, **kwargs)
+ return decorator
+
+def second_route_beforeware(f):
+ @wraps(f)
+ async def decorator(*args, **kwargs):
+ print('Second route level beforeware')
+ return await f(*args, **kwargs)
+ return decorator
+
+@app.get("/users")
+@route_beforeware
+@second_route_beforeware
+async def users():
+ return "Users Page"
+
+client.get('/users').text
+```
+
+ App level beforeware
+ Route level beforeware
+ Second route level beforeware
+
+ 'Users Page'
+
+This flexiblity allows for granular control over route behaviour,
+enabling you to tailor each endpoint’s functionality as needed. While
+app-level beforeware remains useful for global operations, decorators
+provide a powerful tool for route-specific customization.
+
+## Combining Routes
+
+Sometimes a FastHTML project can grow so weildy that putting all the
+routes into `main.py` becomes unweildy. Or, we install a FastHTML- or
+Starlette-based package that requires us to add routes.
+
+First let’s create a `books.py` module, that represents all the
+user-related views:
+
+``` python
+# books.py
+books_app, rt = fast_app()
+
+books = ['A Guide to FastHTML', 'FastHTML Cookbook', 'FastHTML in 24 Hours']
+
+@rt("/", name="list")
+def get():
+ return Titled("Books", *[P(book) for book in books])
+```
+
+Let’s mount it in our main module:
+
+``` python
+from books import app as books_app
+
+app, rt = fast_app(routes=[Mount("/books", books_app, name="books")])
+
+@rt("/")
+def get():
+ return Titled("Dashboard",
+ P(A(href="/books")("Books")),
+ Hr(),
+ P(A(link=uri("books:list"))("Books")),
+ )
+
+serve()
+```
+
+Line 3
+We use `starlette.Mount` to add the route to our routes list. We provide
+the name of `books` to make discovery and management of the links
+easier. More on that in items 2 and 3 of this annotations list
+
+Line 8
+This example link to the books list view is hand-crafted. Obvious in
+purpose, it makes changing link patterns in the future harder
+
+Line 10
+This example link uses the named URL route for the books. The advantage
+of this approach is it makes management of large numbers of link items
+easier.
diff --git a/explains/websockets.html b/explains/websockets.html
new file mode 100644
index 00000000..332b9df4
--- /dev/null
+++ b/explains/websockets.html
@@ -0,0 +1,937 @@
+
+
+
+
+
+
+
+
+
+WebSockets – fasthtml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Websockets are a protocol for two-way, persistent communication between a client and server. This is different from HTTP, which uses a request/response model where the client sends a request and the server responds. With websockets, either party can send messages at any time, and the other party can respond.
+
This allows for different applications to be built, including things like chat apps, live-updating dashboards, and real-time collaborative tools, which would require constant polling of the server for updates with HTTP.
+
In FastHTML, you can create a websocket route using the @app.ws decorator. This decorator takes a route path, and optional conn and disconn parameters representing the on_connect and on_disconnect callbacks in websockets, respectively. The function decorated by @app.ws is the main function that is called when a message is received.
The on_message function is the main function that is called when a message is received and can be named however you like. Similar to standard routes, the arguments to on_message are automatically parsed from the websocket payload for you, so you don’t need to manually parse the message content. However, certain argument names are reserved for special purposes. Here are the most important ones:
+
+
send is a function that can be used to send text data to the client.
+
data is a dictionary containing the data sent by the client.
+
ws is a reference to the websocket object.
+
+
For example, we can send a message to the client that just connected like this:
This will create a websocket connection to the server on route /ws, and send any form submissions to the server via the websocket. The server will then respond by sending a message back to the client. The client will then update the message div with the message from the server using Out of Band Swaps, which means that the content is swapped with the same id without reloading the page.
+
+
+
+
+
+
+Note
+
+
+
+
Make sure you set ws_hdr=True when creating your FastHTML object if you want to use websockets so the extension is loaded.
+
+
+
Putting it all together, the code for the client and server should look like this:
This is a fairly simple example and could be done just as easily with standard HTTP requests, but it illustrates the basic idea of how websockets work. Let’s look at a more complex example next.
+
+
Real-Time Chat App
+
Let’s put our new websocket knowledge to use by building a simple chat app. We will create a chat app where multiple users can send and receive messages in real time.
+
Let’s start by defining the app and the home page:
+
from fasthtml.common import*
+
+app = FastHTML(ws_hdr=True)
+rt = app.route
+
+msgs = []
+@rt('/')
+def home(): return Div(
+ Div(Ul(*[Li(m) for m in msgs], id='msg-list')),
+ Form(Input(id='msg'), id='form', ws_send=True),
+ hx_ext='ws', ws_connect='/ws')
+
Now, let’s handle the websocket connection. We’ll add a new route for this along with an on_conn and on_disconn function to keep track of the users currently connected to the websocket. Finally, we will handle the logic for sending messages to all connected users.
+
users = {}
+def on_conn(ws, send): users[str(id(ws))] = send
+def on_disconn(ws): users.pop(str(id(ws)), None)
+
+@app.ws('/ws', conn=on_conn, disconn=on_disconn)
+asyncdef ws(msg:str):
+ msgs.append(msg)
+# Use associated `send` function to send message to each user
+for u in users.values(): await u(Ul(*[Li(m) for m in msgs], id='msg-list'))
+
+serve()
+
We can now run this app with python chat_ws.py and open multiple browser tabs to http://localhost:5001. You should be able to send messages in one tab and see them appear in the other tabs.
+
+
A Work in Progress
+
This page (and Websocket support in FastHTML) is a work in progress. Questions, PRs, and feedback are welcome!
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/explains/websockets.html.md b/explains/websockets.html.md
new file mode 100644
index 00000000..61f498de
--- /dev/null
+++ b/explains/websockets.html.md
@@ -0,0 +1,173 @@
+# WebSockets
+
+
+
+
+Websockets are a protocol for two-way, persistent communication between
+a client and server. This is different from HTTP, which uses a
+request/response model where the client sends a request and the server
+responds. With websockets, either party can send messages at any time,
+and the other party can respond.
+
+This allows for different applications to be built, including things
+like chat apps, live-updating dashboards, and real-time collaborative
+tools, which would require constant polling of the server for updates
+with HTTP.
+
+In FastHTML, you can create a websocket route using the `@app.ws`
+decorator. This decorator takes a route path, and optional `conn` and
+`disconn` parameters representing the `on_connect` and `on_disconnect`
+callbacks in websockets, respectively. The function decorated by
+`@app.ws` is the main function that is called when a message is
+received.
+
+Here’s an example of a basic websocket route:
+
+``` python
+@app.ws('/ws', conn=on_conn, disconn=on_disconn)
+async def on_message(msg:str, send):
+ await send(Div('Hello ' + msg, id='notifications'))
+ await send(Div('Goodbye ' + msg, id='notifications'))
+```
+
+The `on_message` function is the main function that is called when a
+message is received and can be named however you like. Similar to
+standard routes, the arguments to `on_message` are automatically parsed
+from the websocket payload for you, so you don’t need to manually parse
+the message content. However, certain argument names are reserved for
+special purposes. Here are the most important ones:
+
+- `send` is a function that can be used to send text data to the client.
+- `data` is a dictionary containing the data sent by the client.
+- `ws` is a reference to the websocket object.
+
+For example, we can send a message to the client that just connected
+like this:
+
+``` python
+async def on_conn(send):
+ await send(Div('Hello, world!'))
+```
+
+Or if we receive a message from the client, we can send a message back
+to them:
+
+``` python
+@app.ws('/ws', conn=on_conn, disconn=on_disconn)
+async def on_message(msg:str, send):
+ await send(Div('You said: ' + msg, id='notifications'))
+ # or...
+ return Div('You said: ' + msg, id='notifications')
+```
+
+On the client side, we can use HTMX’s websocket extension to open a
+websocket connection and send/receive messages. For example:
+
+``` python
+from fasthtml.common import *
+
+app = FastHTML(ws_hdr=True)
+
+@app.get('/')
+def home():
+ cts = Div(
+ Div(id='notifications'),
+ Form(Input(id='msg'), id='form', ws_send=True),
+ hx_ext='ws', ws_connect='/ws')
+ return Titled('Websocket Test', cts)
+```
+
+This will create a websocket connection to the server on route `/ws`,
+and send any form submissions to the server via the websocket. The
+server will then respond by sending a message back to the client. The
+client will then update the message div with the message from the server
+using Out of Band Swaps, which means that the content is swapped with
+the same id without reloading the page.
+
+
+
+> **Note**
+>
+> Make sure you set `ws_hdr=True` when creating your
+> [`FastHTML`](https://AnswerDotAI.github.io/fasthtml/api/core.html#fasthtml)
+> object if you want to use websockets so the extension is loaded.
+
+
+
+Putting it all together, the code for the client and server should look
+like this:
+
+``` python
+from fasthtml.common import *
+
+app = FastHTML(ws_hdr=True)
+rt = app.route
+
+@rt('/')
+def get():
+ cts = Div(
+ Div(id='notifications'),
+ Form(Input(id='msg'), id='form', ws_send=True),
+ hx_ext='ws', ws_connect='/ws')
+ return Titled('Websocket Test', cts)
+
+@app.ws('/ws')
+async def ws(msg:str, send):
+ await send(Div('Hello ' + msg, id='notifications'))
+
+serve()
+```
+
+This is a fairly simple example and could be done just as easily with
+standard HTTP requests, but it illustrates the basic idea of how
+websockets work. Let’s look at a more complex example next.
+
+## Real-Time Chat App
+
+Let’s put our new websocket knowledge to use by building a simple chat
+app. We will create a chat app where multiple users can send and receive
+messages in real time.
+
+Let’s start by defining the app and the home page:
+
+``` python
+from fasthtml.common import *
+
+app = FastHTML(ws_hdr=True)
+rt = app.route
+
+msgs = []
+@rt('/')
+def home(): return Div(
+ Div(Ul(*[Li(m) for m in msgs], id='msg-list')),
+ Form(Input(id='msg'), id='form', ws_send=True),
+ hx_ext='ws', ws_connect='/ws')
+```
+
+Now, let’s handle the websocket connection. We’ll add a new route for
+this along with an `on_conn` and `on_disconn` function to keep track of
+the users currently connected to the websocket. Finally, we will handle
+the logic for sending messages to all connected users.
+
+``` python
+users = {}
+def on_conn(ws, send): users[str(id(ws))] = send
+def on_disconn(ws): users.pop(str(id(ws)), None)
+
+@app.ws('/ws', conn=on_conn, disconn=on_disconn)
+async def ws(msg:str):
+ msgs.append(msg)
+ # Use associated `send` function to send message to each user
+ for u in users.values(): await u(Ul(*[Li(m) for m in msgs], id='msg-list'))
+
+serve()
+```
+
+We can now run this app with `python chat_ws.py` and open multiple
+browser tabs to `http://localhost:5001`. You should be able to send
+messages in one tab and see them appear in the other tabs.
+
+### A Work in Progress
+
+This page (and Websocket support in FastHTML) is a work in progress.
+Questions, PRs, and feedback are welcome!
diff --git a/favicon.ico b/favicon.ico
new file mode 100644
index 00000000..82ef3bab
Binary files /dev/null and b/favicon.ico differ
diff --git a/index.html b/index.html
new file mode 100644
index 00000000..c5af3030
--- /dev/null
+++ b/index.html
@@ -0,0 +1,920 @@
+
+
+
+
+
+
+
+
+
+
+FastHTML – fasthtml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ The fastest, most powerful way to create an HTML app
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Welcome to the official FastHTML documentation.
+
FastHTML is a new next-generation web framework for fast, scalable web applications with minimal, compact code. It’s designed to be:
+
+
Powerful and expressive enough to build the most advanced, interactive web apps you can imagine.
+
Fast and lightweight, so you can write less code and get more done.
+
Easy to learn and use, with a simple, intuitive syntax that makes it easy to build complex apps quickly.
+
+
FastHTML apps are just Python code, so you can use FastHTML with the full power of the Python language and ecosystem. FastHTML’s functionality maps 1:1 directly to HTML and HTTP, but allows them to be encapsulated using good software engineering practices—so you’ll need to understand these foundations to use this library fully. To understand how and why this works, please read this first: about.fastht.ml.
+
+
Installation
+
Since fasthtml is a Python library, you can install it with:
+
pip install python-fasthtml
+
In the near future, we hope to add component libraries that can likewise be installed via pip.
+
+
+
Usage
+
For a minimal app, create a file “main.py” as follows:
Running the app with python main.py prints out a link to your running app: http://localhost:5001. Visit that link in your browser and you should see a page with the text “Hello World!”. Congratulations, you’ve just created your first FastHTML app!
+
Adding interactivity is surprisingly easy, thanks to HTMX. Modify the file to add this function:
+
+
+
main.py
+
+
@rt('/change')
+def get(): return P('Nice to be here!')
+
+
You now have a page with a clickable element that changes the text when clicked. When clicking on this link, the server will respond with an “HTML partial”—that is, just a snippet of HTML which will be inserted into the existing page. In this case, the returned element will replace the original P element (since that’s the default behavior of HTMX) with the new version returned by the second route.
+
This “hypermedia-based” approach to web development is a powerful way to build web applications.
+
+
Getting help from AI
+
Because FastHTML is newer than most LLMs, AI systems like Cursor, ChatGPT, Claude, and Copilot won’t give useful answers about it. To fix that problem, we’ve provided an LLM-friendly guide that teaches them how to use FastHTML. To use it, add this link for your AI helper to use:
This example is in a format based on recommendations from Anthropic for use with Claude Projects. This works so well that we’ve actually found that Claude can provide even better information than our own documentation! For instance, read through this annotated Claude chat for some great getting-started information, entirely generated from a project using the above text file as context.
+
If you use Cursor, type @doc then choose “Add new doc”, and use the /llms-ctx.txt link above. The context file is auto-generated from our llms.txt (our proposed standard for providing AI-friendly information)—you can generate alternative versions suitable for other models as needed.
+
+
+
+
Next Steps
+
Start with the official sources to learn more about FastHTML:
Documentation: Learn from examples how to write FastHTML code
+
Idiomatic app: Heavily commented source code walking through a complete application, including custom authentication, JS library connections, and database use.
+
+
We also have a 1-hour intro video:
+
+
The capabilities of FastHTML are vast and growing, and not all the features and patterns have been documented yet. Be prepared to invest time into studying and modifying source code, such as the main FastHTML repo’s notebooks and the official FastHTML examples repo:
+
+
+
+
+
\ No newline at end of file
diff --git a/index.html.md b/index.html.md
new file mode 100644
index 00000000..acbcd71b
--- /dev/null
+++ b/index.html.md
@@ -0,0 +1,152 @@
+# FastHTML
+
+
+
+
+Welcome to the official FastHTML documentation.
+
+FastHTML is a new next-generation web framework for fast, scalable web
+applications with minimal, compact code. It’s designed to be:
+
+- Powerful and expressive enough to build the most advanced, interactive
+ web apps you can imagine.
+- Fast and lightweight, so you can write less code and get more done.
+- Easy to learn and use, with a simple, intuitive syntax that makes it
+ easy to build complex apps quickly.
+
+FastHTML apps are just Python code, so you can use FastHTML with the
+full power of the Python language and ecosystem. FastHTML’s
+functionality maps 1:1 directly to HTML and HTTP, but allows them to be
+encapsulated using good software engineering practices—so you’ll need to
+understand these foundations to use this library fully. To understand
+how and why this works, please read this first:
+[about.fastht.ml](https://about.fastht.ml/).
+
+## Installation
+
+Since `fasthtml` is a Python library, you can install it with:
+
+``` sh
+pip install python-fasthtml
+```
+
+In the near future, we hope to add component libraries that can likewise
+be installed via `pip`.
+
+## Usage
+
+For a minimal app, create a file “main.py” as follows:
+
+
+
+Running the app with `python main.py` prints out a link to your running
+app: `http://localhost:5001`. Visit that link in your browser and you
+should see a page with the text “Hello World!”. Congratulations, you’ve
+just created your first FastHTML app!
+
+Adding interactivity is surprisingly easy, thanks to HTMX. Modify the
+file to add this function:
+
+
+
+**main.py**
+
+``` python
+@rt('/change')
+def get(): return P('Nice to be here!')
+```
+
+
+
+You now have a page with a clickable element that changes the text when
+clicked. When clicking on this link, the server will respond with an
+“HTML partial”—that is, just a snippet of HTML which will be inserted
+into the existing page. In this case, the returned element will replace
+the original P element (since that’s the default behavior of HTMX) with
+the new version returned by the second route.
+
+This “hypermedia-based” approach to web development is a powerful way to
+build web applications.
+
+### Getting help from AI
+
+Because FastHTML is newer than most LLMs, AI systems like Cursor,
+ChatGPT, Claude, and Copilot won’t give useful answers about it. To fix
+that problem, we’ve provided an LLM-friendly guide that teaches them how
+to use FastHTML. To use it, add this link for your AI helper to use:
+
+- [/llms-ctx.txt](https://docs.fastht.ml/llms-ctx.txt)
+
+This example is in a format based on recommendations from Anthropic for
+use with [Claude
+Projects](https://support.anthropic.com/en/articles/9517075-what-are-projects).
+This works so well that we’ve actually found that Claude can provide
+even better information than our own documentation! For instance, read
+through [this annotated Claude
+chat](https://gist.github.com/jph00/9559b0a563f6a370029bec1d1cc97b74)
+for some great getting-started information, entirely generated from a
+project using the above text file as context.
+
+If you use Cursor, type `@doc` then choose “*Add new doc*”, and use the
+/llms-ctx.txt link above. The context file is auto-generated from our
+[`llms.txt`](https://llmstxt.org/) (our proposed standard for providing
+AI-friendly information)—you can generate alternative versions suitable
+for other models as needed.
+
+## Next Steps
+
+Start with the official sources to learn more about FastHTML:
+
+- [About](https://about.fastht.ml): Learn about the core ideas behind
+ FastHTML
+- [Documentation](https://docs.fastht.ml): Learn from examples how to
+ write FastHTML code
+- [Idiomatic
+ app](https://github.com/AnswerDotAI/fasthtml/blob/main/examples/adv_app.py):
+ Heavily commented source code walking through a complete application,
+ including custom authentication, JS library connections, and database
+ use.
+
+We also have a 1-hour intro video:
+
+
+
+The capabilities of FastHTML are vast and growing, and not all the
+features and patterns have been documented yet. Be prepared to invest
+time into studying and modifying source code, such as the main FastHTML
+repo’s notebooks and the official FastHTML examples repo:
+
+- [FastHTML Examples Repo on
+ GitHub](https://github.com/AnswerDotAI/fasthtml-example)
+- [FastHTML Repo on GitHub](https://github.com/AnswerDotAI/fasthtml)
+
+Then explore the small but growing third-party ecosystem of FastHTML
+tutorials, notebooks, libraries, and components:
+
+- [FastHTML Gallery](https://gallery.fastht.ml): Learn from minimal
+ examples of components (ie chat bubbles, click-to-edit, infinite
+ scroll, etc)
+- [Creating Custom FastHTML Tags for Markdown
+ Rendering](https://isaac-flath.github.io/website/posts/boots/FasthtmlTutorial.html)
+ by Isaac Flath
+- Your tutorial here!
+
+Finally, join the FastHTML community to ask questions, share your work,
+and learn from others:
+
+- [Discord](https://discord.gg/qcXvcxMhdP)
diff --git a/listings.json b/listings.json
new file mode 100644
index 00000000..0f6cf2ba
--- /dev/null
+++ b/listings.json
@@ -0,0 +1,11 @@
+[
+ {
+ "listing": "/tutorials/index.html",
+ "items": [
+ "/tutorials/by_example.html",
+ "/tutorials/quickstart_for_web_devs.html",
+ "/tutorials/e2e.html",
+ "/tutorials/jupyter_and_fasthtml.html"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/llms-ctx-full.txt b/llms-ctx-full.txt
new file mode 100644
index 00000000..5ec67a68
--- /dev/null
+++ b/llms-ctx-full.txt
@@ -0,0 +1,4637 @@
+Things to remember when writing FastHTML apps:
+
+- Although parts of its API are inspired by FastAPI, it is *not* compatible with FastAPI syntax and is not targeted at creating API services
+- FastHTML includes support for Pico CSS and the fastlite sqlite library, although using both are optional; sqlalchemy can be used directly or via the fastsql library, and any CSS framework can be used. Support for the Surreal and css-scope-inline libraries are also included, but both are optional
+- FastHTML is compatible with JS-native web components and any vanilla JS library, but not with React, Vue, or Svelte
+- Use `serve()` for running uvicorn (`if __name__ == "__main__"` is not needed since it's automatic)
+- When a title is needed with a response, use `Titled`; note that that already wraps children in `Container`, and already includes both the meta title as well as the H1 element.# Web Devs Quickstart
+
+
+
+## Installation
+
+``` bash
+pip install python-fasthtml
+```
+
+## A Minimal Application
+
+A minimal FastHTML application looks something like this:
+
+
+
+Line 1
+We import what we need for rapid development! A carefully-curated set of
+FastHTML functions and other Python objects is brought into our global
+namespace for convenience.
+
+Line 3
+We instantiate a FastHTML app with the `fast_app()` utility function.
+This provides a number of really useful defaults that we’ll take
+advantage of later in the tutorial.
+
+Line 5
+We use the `rt()` decorator to tell FastHTML what to return when a user
+visits `/` in their browser.
+
+Line 6
+We connect this route to HTTP GET requests by defining a view function
+called `get()`.
+
+Line 7
+A tree of Python function calls that return all the HTML required to
+write a properly formed web page. You’ll soon see the power of this
+approach.
+
+Line 9
+The `serve()` utility configures and runs FastHTML using a library
+called `uvicorn`.
+
+Run the code:
+
+``` bash
+python main.py
+```
+
+The terminal will look like this:
+
+``` bash
+INFO: Uvicorn running on http://0.0.0.0:5001 (Press CTRL+C to quit)
+INFO: Started reloader process [58058] using WatchFiles
+INFO: Started server process [58060]
+INFO: Waiting for application startup.
+INFO: Application startup complete.
+```
+
+Confirm FastHTML is running by opening your web browser to
+[127.0.0.1:5001](http://127.0.0.1:5001). You should see something like
+the image below:
+
+![](quickstart-web-dev/quickstart-fasthtml.png)
+
+
+
+> **Note**
+>
+> While some linters and developers will complain about the wildcard
+> import, it is by design here and perfectly safe. FastHTML is very
+> deliberate about the objects it exports in `fasthtml.common`. If it
+> bothers you, you can import the objects you need individually, though
+> it will make the code more verbose and less readable.
+>
+> If you want to learn more about how FastHTML handles imports, we cover
+> that [here](https://docs.fastht.ml/explains/faq.html#why-use-import).
+
+
+
+## A Minimal Charting Application
+
+The
+[`Script`](https://AnswerDotAI.github.io/fasthtml/api/xtend.html#script)
+function allows you to include JavaScript. You can use Python to
+generate parts of your JS or JSON like this:
+
+``` python
+import json
+from fasthtml.common import *
+
+app, rt = fast_app(hdrs=(Script(src="https://cdn.plot.ly/plotly-2.32.0.min.js"),))
+
+data = json.dumps({
+ "data": [{"x": [1, 2, 3, 4],"type": "scatter"},
+ {"x": [1, 2, 3, 4],"y": [16, 5, 11, 9],"type": "scatter"}],
+ "title": "Plotly chart in FastHTML ",
+ "description": "This is a demo dashboard",
+ "type": "scatter"
+})
+
+
+@rt("/")
+def get():
+ return Titled("Chart Demo", Div(id="myDiv"),
+ Script(f"var data = {data}; Plotly.newPlot('myDiv', data);"))
+
+serve()
+```
+
+## Debug Mode
+
+When we can’t figure out a bug in FastHTML, we can run it in `DEBUG`
+mode. When an error is thrown, the error screen is displayed in the
+browser. This error setting should never be used in a deployed app.
+
+``` python
+from fasthtml.common import *
+
+app, rt = fast_app(debug=True)
+
+@rt("/")
+def get():
+ 1/0
+ return Titled("FastHTML Error!", P("Let's error!"))
+
+serve()
+```
+
+Line 3
+`debug=True` sets debug mode on.
+
+Line 7
+Python throws an error when it tries to divide an integer by zero.
+
+## Routing
+
+FastHTML builds upon FastAPI’s friendly decorator pattern for specifying
+URLs, with extra features:
+
+
+
+Line 5
+The “/” URL on line 5 is the home of a project. This would be accessed
+at [127.0.0.1:5001](http://127.0.0.1:5001).
+
+Line 9
+“/hello” URL on line 9 will be found by the project if the user visits
+[127.0.0.1:5001/hello](http://127.0.0.1:5001/hello).
+
+
+
+> **Tip**
+>
+> It looks like `get()` is being defined twice, but that’s not the case.
+> Each function decorated with `rt` is totally separate, and is injected
+> into the router. We’re not calling them in the module’s namespace
+> (`locals()`). Rather, we’re loading them into the routing mechanism
+> using the `rt` decorator.
+
+
+
+You can do more! Read on to learn what we can do to make parts of the
+URL dynamic.
+
+## Variables in URLs
+
+You can add variable sections to a URL by marking them with
+`{variable_name}`. Your function then receives the `{variable_name}` as
+a keyword argument, but only if it is the correct type. Here’s an
+example:
+
+
+
+Line 5
+We specify two variable names, `name` and `age`.
+
+Line 6
+We define two function arguments named identically to the variables. You
+will note that we specify the Python types to be passed.
+
+Line 7
+We use these functions in our project.
+
+Try it out by going to this address:
+[127.0.0.1:5001/uma/5](http://127.0.0.1:5001/uma/5). You should get a
+page that says,
+
+> “Hello Uma, age 5”.
+
+### What happens if we enter incorrect data?
+
+The [127.0.0.1:5001/uma/5](http://127.0.0.1:5001/uma/5) URL works
+because `5` is an integer. If we enter something that is not, such as
+[127.0.0.1:5001/uma/five](http://127.0.0.1:5001/uma/five), then FastHTML
+will return an error instead of a web page.
+
+
+
+> **FastHTML URL routing supports more complex types**
+>
+> The two examples we provide here use Python’s built-in `str` and `int`
+> types, but you can use your own types, including more complex ones
+> such as those defined by libraries like
+> [attrs](https://pypi.org/project/attrs/),
+> [pydantic](https://pypi.org/project/pydantic/), and even
+> [sqlmodel](https://pypi.org/project/sqlmodel/).
+
+
+
+## HTTP Methods
+
+FastHTML matches function names to HTTP methods. So far the URL routes
+we’ve defined have been for HTTP GET methods, the most common method for
+web pages.
+
+Form submissions often are sent as HTTP POST. When dealing with more
+dynamic web page designs, also known as Single Page Apps (SPA for
+short), the need can arise for other methods such as HTTP PUT and HTTP
+DELETE. The way FastHTML handles this is by changing the function name.
+
+
+
+Line 6
+On line 6 because the `get()` function name is used, this will handle
+HTTP GETs going to the `/` URI.
+
+Line 10
+On line 10 because the `post()` function name is used, this will handle
+HTTP POSTs going to the `/` URI.
+
+## CSS Files and Inline Styles
+
+Here we modify default headers to demonstrate how to use the [Sakura CSS
+microframework](https://github.com/oxalorg/sakura) instead of FastHTML’s
+default of Pico CSS.
+
+
+
+Line 4
+By setting `pico` to `False`, FastHTML will not include `pico.min.css`.
+
+Line 7
+This will generate an HTML `` tag for sourcing the css for Sakura.
+
+Line 8
+If you want an inline styles, the `Style()` function will put the result
+into the HTML.
+
+## Other Static Media File Locations
+
+As you saw,
+[`Script`](https://AnswerDotAI.github.io/fasthtml/api/xtend.html#script)
+and `Link` are specific to the most common static media use cases in web
+apps: including JavaScript, CSS, and images. But it also works with
+videos and other static media files. The default behavior is to look for
+these files in the root directory - typically we don’t do anything
+special to include them. We can change the default directory that is
+looked in for files by adding the `static_path` parameter to the
+[`fast_app`](https://AnswerDotAI.github.io/fasthtml/api/fastapp.html#fast_app)
+function.
+
+``` python
+app, rt = fast_app(static_path='public')
+```
+
+FastHTML also allows us to define a route that uses `FileResponse` to
+serve the file at a specified path. This is useful for serving images,
+videos, and other media files from a different directory without having
+to change the paths of many files. So if we move the directory
+containing the media files, we only need to change the path in one
+place. In the example below, we call images from a directory called
+`public`.
+
+``` python
+@rt("/{fname:path}.{ext:static}")
+async def get(fname:str, ext:str):
+ return FileResponse(f'public/{fname}.{ext}')
+```
+
+## Rendering Markdown
+
+``` python
+from fasthtml.common import *
+
+hdrs = (MarkdownJS(), HighlightJS(langs=['python', 'javascript', 'html', 'css']), )
+
+app, rt = fast_app(hdrs=hdrs)
+
+content = """
+Here are some _markdown_ elements.
+
+- This is a list item
+- This is another list item
+- And this is a third list item
+
+**Fenced code blocks work here.**
+"""
+
+@rt('/')
+def get(req):
+ return Titled("Markdown rendering example", Div(content,cls="marked"))
+
+serve()
+```
+
+## Code highlighting
+
+Here’s how to highlight code without any markdown configuration.
+
+``` python
+from fasthtml.common import *
+
+# Add the HighlightJS built-in header
+hdrs = (HighlightJS(langs=['python', 'javascript', 'html', 'css']),)
+
+app, rt = fast_app(hdrs=hdrs)
+
+code_example = """
+import datetime
+import time
+
+for i in range(10):
+ print(f"{datetime.datetime.now()}")
+ time.sleep(1)
+"""
+
+@rt('/')
+def get(req):
+ return Titled("Markdown rendering example",
+ Div(
+ # The code example needs to be surrounded by
+ # Pre & Code elements
+ Pre(Code(code_example))
+ ))
+
+serve()
+```
+
+## Defining new `ft` components
+
+We can build our own `ft` components and combine them with other
+components. The simplest method is defining them as a function.
+
+``` python
+from fasthtml.common import *
+```
+
+``` python
+def hero(title, statement):
+ return Div(H1(title),P(statement), cls="hero")
+
+# usage example
+Main(
+ hero("Hello World", "This is a hero statement")
+)
+```
+
+``` html
+
+
+
Hello World
+
This is a hero statement
+
+
+```
+
+### Pass through components
+
+For when we need to define a new component that allows zero-to-many
+components to be nested within them, we lean on Python’s `*args` and
+`**kwargs` mechanism. Useful for creating page layout controls.
+
+``` python
+def layout(*args, **kwargs):
+ """Dashboard layout for all our dashboard views"""
+ return Main(
+ H1("Dashboard"),
+ Div(*args, **kwargs),
+ cls="dashboard",
+ )
+
+# usage example
+layout(
+ Ul(*[Li(o) for o in range(3)]),
+ P("Some content", cls="description"),
+)
+```
+
+``` html
+
+
Dashboard
+
+
+
0
+
1
+
2
+
+
Some content
+
+
+```
+
+### Dataclasses as ft components
+
+While functions are easy to read, for more complex components some might
+find it easier to use a dataclass.
+
+``` python
+from dataclasses import dataclass
+
+@dataclass
+class Hero:
+ title: str
+ statement: str
+
+ def __ft__(self):
+ """ The __ft__ method renders the dataclass at runtime."""
+ return Div(H1(self.title),P(self.statement), cls="hero")
+
+# usage example
+Main(
+ Hero("Hello World", "This is a hero statement")
+)
+```
+
+``` html
+
+
+
Hello World
+
This is a hero statement
+
+
+```
+
+## Testing views in notebooks
+
+Because of the ASGI event loop it is currently impossible to run
+FastHTML inside a notebook. However, we can still test the output of our
+views. To do this, we leverage Starlette, an ASGI toolkit that FastHTML
+uses.
+
+``` python
+# First we instantiate our app, in this case we remove the
+# default headers to reduce the size of the output.
+app, rt = fast_app(default_hdrs=False)
+
+# Setting up the Starlette test client
+from starlette.testclient import TestClient
+client = TestClient(app)
+
+# Usage example
+@rt("/")
+def get():
+ return Titled("FastHTML is awesome",
+ P("The fastest way to create web apps in Python"))
+
+print(client.get("/").text)
+```
+
+
+
+
+
+ FastHTML is awesome
+
+
+
+
FastHTML is awesome
+
The fastest way to create web apps in Python
+
+
+
+
+## Forms
+
+To validate data coming from users, first define a dataclass
+representing the data you want to check. Here’s an example representing
+a signup form.
+
+``` python
+from dataclasses import dataclass
+
+@dataclass
+class Profile: email:str; phone:str; age:int
+```
+
+Create an FT component representing an empty version of that form. Don’t
+pass in any value to fill the form, that gets handled later.
+
+``` python
+profile_form = Form(method="post", action="/profile")(
+ Fieldset(
+ Label('Email', Input(name="email")),
+ Label("Phone", Input(name="phone")),
+ Label("Age", Input(name="age")),
+ ),
+ Button("Save", type="submit"),
+ )
+profile_form
+```
+
+``` html
+
+```
+
+Once the dataclass and form function are completed, we can add data to
+the form. To do that, instantiate the profile dataclass:
+
+``` python
+profile = Profile(email='john@example.com', phone='123456789', age=5)
+profile
+```
+
+ Profile(email='john@example.com', phone='123456789', age=5)
+
+Then add that data to the `profile_form` using FastHTML’s
+[`fill_form`](https://AnswerDotAI.github.io/fasthtml/api/components.html#fill_form)
+class:
+
+``` python
+fill_form(profile_form, profile)
+```
+
+``` html
+
+```
+
+### Forms with views
+
+The usefulness of FastHTML forms becomes more apparent when they are
+combined with FastHTML views. We’ll show how this works by using the
+test client from above. First, let’s create a SQlite database:
+
+``` python
+db = Database("profiles.db")
+profiles = db.create(Profile, pk="email")
+```
+
+Now we insert a record into the database:
+
+``` python
+profiles.insert(profile)
+```
+
+ Profile(email='john@example.com', phone='123456789', age=5)
+
+And we can then demonstrate in the code that form is filled and
+displayed to the user.
+
+``` python
+@rt("/profile/{email}")
+def profile(email:str):
+ profile = profiles[email]
+ filled_profile_form = fill_form(profile_form, profile)
+ return Titled(f'Profile for {profile.email}', filled_profile_form)
+
+print(client.get(f"/profile/john@example.com").text)
+```
+
+Line 3
+Fetch the profile using the profile table’s `email` primary key
+
+Line 4
+Fill the form for display.
+
+
+
+
+
+
+ Profile for john@example.com
+
+
+
+
Profile for john@example.com
+
+
+
+
+
+And now let’s demonstrate making a change to the data.
+
+``` python
+@rt("/profile")
+def post(profile: Profile):
+ profiles.update(profile)
+ return RedirectResponse(url=f"/profile/{profile.email}")
+
+new_data = dict(email='john@example.com', phone='7654321', age=25)
+print(client.post("/profile", data=new_data).text)
+```
+
+Line 2
+We use the `Profile` dataclass definition to set the type for the
+incoming `profile` content. This validates the field types for the
+incoming data
+
+Line 3
+Taking our validated data, we updated the profiles table
+
+Line 4
+We redirect the user back to their profile view
+
+Line 7
+The display is of the profile form view showing the changes in data.
+
+
+
+
+
+
+ Profile for john@example.com
+
+
+
+
Profile for john@example.com
+
+
+
+
+
+## Strings and conversion order
+
+The general rules for rendering are: - `__ft__` method will be called
+(for default components like `P`, `H2`, etc. or if you define your own
+components) - If you pass a string, it will be escaped - On other python
+objects, `str()` will be called
+
+As a consequence, if you want to include plain HTML tags directly into
+e.g. a `Div()` they will get escaped by default (as a security measure
+to avoid code injections). This can be avoided by using `NotStr()`, a
+convenient way to reuse python code that returns already HTML. If you
+use pandas, you can use `pandas.DataFrame.to_html()` to get a nice
+table. To include the output a FastHTML, wrap it in `NotStr()`, like
+`Div(NotStr(df.to_html()))`.
+
+Above we saw how a dataclass behaves with the `__ft__` method defined.
+On a plain dataclass, `str()` will be called (but not escaped).
+
+``` python
+from dataclasses import dataclass
+
+@dataclass
+class Hero:
+ title: str
+ statement: str
+
+# rendering the dataclass with the default method
+Main(
+ Hero("
Hello World
", "This is a hero statement")
+)
+```
+
+``` html
+Hero(title='
Hello World
', statement='This is a hero statement')
+```
+
+``` python
+# This will display the HTML as text on your page
+Div("Let's include some HTML here:
Some HTML
")
+```
+
+``` html
+
Let's include some HTML here: <div>Some HTML</div>
+```
+
+``` python
+# Keep the string untouched, will be rendered on the page
+Div(NotStr("
Some HTML
"))
+```
+
+``` html
+
Some HTML
+```
+
+## Custom exception handlers
+
+FastHTML allows customization of exception handlers, but does so
+gracefully. What this means is by default it includes all the ``
+tags needed to display attractive content. Try it out!
+
+``` python
+from fasthtml.common import *
+
+def not_found(req, exc): return Titled("404: I don't exist!")
+
+exception_handlers = {404: not_found}
+
+app, rt = fast_app(exception_handlers=exception_handlers)
+
+@rt('/')
+def get():
+ return (Titled("Home page", P(A(href="/oops")("Click to generate 404 error"))))
+
+serve()
+```
+
+We can also use lambda to make things more terse:
+
+``` python
+from fasthtml.common import *
+
+exception_handlers={
+ 404: lambda req, exc: Titled("404: I don't exist!"),
+ 418: lambda req, exc: Titled("418: I'm a teapot!")
+}
+
+app, rt = fast_app(exception_handlers=exception_handlers)
+
+@rt('/')
+def get():
+ return (Titled("Home page", P(A(href="/oops")("Click to generate 404 error"))))
+
+serve()
+```
+
+## Cookies
+
+We can set cookies using the `cookie()` function. In our example, we’ll
+create a `timestamp` cookie.
+
+``` python
+from datetime import datetime
+from IPython.display import HTML
+```
+
+``` python
+@rt("/settimestamp")
+def get(req):
+ now = datetime.now()
+ return P(f'Set to {now}'), cookie('now', datetime.now())
+
+HTML(client.get('/settimestamp').text)
+```
+
+
+
+
+ FastHTML page
+
+
+
Set to 2024-09-04 18:30:34.896373
+
+
+
+Now let’s get it back using the same name for our parameter as the
+cookie name.
+
+``` python
+@rt('/gettimestamp')
+def get(now:parsed_date): return f'Cookie was set at time {now.time()}'
+
+client.get('/gettimestamp').text
+```
+
+ 'Cookie was set at time 18:30:34.896405'
+
+## Sessions
+
+For convenience and security, FastHTML has a mechanism for storing small
+amounts of data in the user’s browser. We can do this by adding a
+`session` argument to routes. FastHTML sessions are Python dictionaries,
+and we can leverage to our benefit. The example below shows how to
+concisely set and get sessions.
+
+``` python
+@rt('/adder/{num}')
+def get(session, num: int):
+ session.setdefault('sum', 0)
+ session['sum'] = session.get('sum') + num
+ return Response(f'The sum is {session["sum"]}.')
+```
+
+## Toasts (also known as Messages)
+
+Toasts, sometimes called “Messages” are small notifications usually in
+colored boxes used to notify users that something has happened. Toasts
+can be of four types:
+
+- info
+- success
+- warning
+- error
+
+Examples toasts might include:
+
+- “Payment accepted”
+- “Data submitted”
+- “Request approved”
+
+Toasts require the use of the `setup_toasts()` function plus every view
+needs these two features:
+
+- The session argument
+- Must return FT components
+
+``` python
+setup_toasts(app)
+
+@rt('/toasting')
+def get(session):
+ # Normally one toast is enough, this allows us to see
+ # different toast types in action.
+ add_toast(session, f"Toast is being cooked", "info")
+ add_toast(session, f"Toast is ready", "success")
+ add_toast(session, f"Toast is getting a bit crispy", "warning")
+ add_toast(session, f"Toast is burning!", "error")
+ return Titled("I like toast")
+```
+
+Line 1
+`setup_toasts` is a helper function that adds toast dependencies.
+Usually this would be declared right after `fast_app()`
+
+Line 4
+Toasts require sessions
+
+Line 11
+Views with Toasts must return FT components.
+
+## Authentication and authorization
+
+In FastHTML the tasks of authentication and authorization are handled
+with Beforeware. Beforeware are functions that run before the route
+handler is called. They are useful for global tasks like ensuring users
+are authenticated or have permissions to access a view.
+
+First, we write a function that accepts a request and session arguments:
+
+``` python
+# Status code 303 is a redirect that can change POST to GET,
+# so it's appropriate for a login page.
+login_redir = RedirectResponse('/login', status_code=303)
+
+def user_auth_before(req, sess):
+ # The `auth` key in the request scope is automatically provided
+ # to any handler which requests it, and can not be injected
+ # by the user using query params, cookies, etc, so it should
+ # be secure to use.
+ auth = req.scope['auth'] = sess.get('auth', None)
+ # If the session key is not there, it redirects to the login page.
+ if not auth: return login_redir
+```
+
+Now we pass our `user_auth_before` function as the first argument into a
+[`Beforeware`](https://AnswerDotAI.github.io/fasthtml/api/core.html#beforeware)
+class. We also pass a list of regular expressions to the `skip`
+argument, designed to allow users to still get to the home and login
+pages.
+
+``` python
+beforeware = Beforeware(
+ user_auth_before,
+ skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css', r'.*\.js', '/login', '/']
+)
+
+app, rt = fast_app(before=beforeware)
+```
+
+## Server-sent events (SSE)
+
+With [server-sent
+events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events),
+it’s possible for a server to send new data to a web page at any time,
+by pushing messages to the web page. Unlike WebSockets, SSE can only go
+in one direction: server to client. SSE is also part of the HTTP
+specification unlike WebSockets which uses its own specification.
+
+FastHTML introduces several tools for working with SSE which are covered
+in the example below. While concise, there’s a lot going on in this
+function so we’ve annotated it quite a bit.
+
+``` python
+import random
+from asyncio import sleep
+from fasthtml.common import *
+
+hdrs=(Script(src="https://unpkg.com/htmx-ext-sse@2.2.1/sse.js"),)
+app,rt = fast_app(hdrs=hdrs)
+
+@rt
+def index():
+ return Titled("SSE Random Number Generator",
+ P("Generate pairs of random numbers, as the list grows scroll downwards."),
+ Div(hx_ext="sse",
+ sse_connect="/number-stream",
+ hx_swap="beforeend show:bottom",
+ sse_swap="message"))
+
+shutdown_event = signal_shutdown()
+
+async def number_generator():
+ while not shutdown_event.is_set():
+ data = Article(random.randint(1, 100))
+ yield sse_message(data)
+ await sleep(1)
+
+@rt("/number-stream")
+async def get(): return EventStream(number_generator())
+```
+
+Line 5
+Import the HTMX SSE extension
+
+Line 12
+Tell HTMX to load the SSE extension
+
+Line 13
+Look at the `/number-stream` endpoint for SSE content
+
+Line 14
+When new items come in from the SSE endpoint, add them at the end of the
+current content within the div. If they go beyond the screen, scroll
+downwards
+
+Line 15
+Specify the name of the event. FastHTML’s default event name is
+“message”. Only change if you have more than one call to SSE endpoints
+within a view
+
+Line 17
+Set up the asyncio event loop
+
+Line 19
+Don’t forget to make this an `async` function!
+
+Line 20
+Iterate through the asyncio event loop
+
+Line 22
+We yield the data. Data ideally should be comprised of FT components as
+that plugs nicely into HTMX in the browser
+
+Line 26
+The endpoint view needs to be an async function that returns a
+[`EventStream`](https://AnswerDotAI.github.io/fasthtml/api/core.html#eventstream)
+
+
+
+> **New content as of September 1, 2024**
+>
+> - [Forms](../tutorials/quickstart_for_web_devs.html#forms)
+> - [Server-side Events
+> (SSE)](../tutorials/quickstart_for_web_devs.html#server-sent-events-sse)
+>
+> We’re going to be adding more to this document, so check back
+> frequently for updates.
+
+
+
+| Attribute | Description |
+|--------------------------------------------------|--------------------------------------------------------------------------------------------------------------------|
+| [`hx-get`](@/attributes/hx-get.md) | issues a `GET` to the specified URL |
+| [`hx-post`](@/attributes/hx-post.md) | issues a `POST` to the specified URL |
+| [`hx-on*`](@/attributes/hx-on.md) | handle events with inline scripts on elements |
+| [`hx-push-url`](@/attributes/hx-push-url.md) | push a URL into the browser location bar to create history |
+| [`hx-select`](@/attributes/hx-select.md) | select content to swap in from a response |
+| [`hx-select-oob`](@/attributes/hx-select-oob.md) | select content to swap in from a response, somewhere other than the target (out of band) |
+| [`hx-swap`](@/attributes/hx-swap.md) | controls how content will swap in (`outerHTML`, `beforeend`, `afterend`, ...) |
+| [`hx-swap-oob`](@/attributes/hx-swap-oob.md) | mark element to swap in from a response (out of band) |
+| [`hx-target`](@/attributes/hx-target.md) | specifies the target element to be swapped |
+| [`hx-trigger`](@/attributes/hx-trigger.md) | specifies the event that triggers the request |
+| [`hx-vals`](@/attributes/hx-vals.md) | add values to submit with the request (JSON format) |
+
+
+
+## Additional Attribute Reference {#attributes-additional}
+
+All other attributes available in htmx.
+
+
+
+| Attribute | Description |
+|------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------|
+| [`hx-boost`](@/attributes/hx-boost.md) | add [progressive enhancement](https://en.wikipedia.org/wiki/Progressive_enhancement) for links and forms |
+| [`hx-confirm`](@/attributes/hx-confirm.md) | shows a `confirm()` dialog before issuing a request |
+| [`hx-delete`](@/attributes/hx-delete.md) | issues a `DELETE` to the specified URL |
+| [`hx-disable`](@/attributes/hx-disable.md) | disables htmx processing for the given node and any children nodes |
+| [`hx-disabled-elt`](@/attributes/hx-disabled-elt.md) | adds the `disabled` attribute to the specified elements while a request is in flight |
+| [`hx-disinherit`](@/attributes/hx-disinherit.md) | control and disable automatic attribute inheritance for child nodes |
+| [`hx-encoding`](@/attributes/hx-encoding.md) | changes the request encoding type |
+| [`hx-ext`](@/attributes/hx-ext.md) | extensions to use for this element |
+| [`hx-headers`](@/attributes/hx-headers.md) | adds to the headers that will be submitted with the request |
+| [`hx-history`](@/attributes/hx-history.md) | prevent sensitive data being saved to the history cache |
+| [`hx-history-elt`](@/attributes/hx-history-elt.md) | the element to snapshot and restore during history navigation |
+| [`hx-include`](@/attributes/hx-include.md) | include additional data in requests |
+| [`hx-indicator`](@/attributes/hx-indicator.md) | the element to put the `htmx-request` class on during the request |
+| [`hx-inherit`](@/attributes/hx-inherit.md) | control and enable automatic attribute inheritance for child nodes if it has been disabled by default |
+| [`hx-params`](@/attributes/hx-params.md) | filters the parameters that will be submitted with a request |
+| [`hx-patch`](@/attributes/hx-patch.md) | issues a `PATCH` to the specified URL |
+| [`hx-preserve`](@/attributes/hx-preserve.md) | specifies elements to keep unchanged between requests |
+| [`hx-prompt`](@/attributes/hx-prompt.md) | shows a `prompt()` before submitting a request |
+| [`hx-put`](@/attributes/hx-put.md) | issues a `PUT` to the specified URL |
+| [`hx-replace-url`](@/attributes/hx-replace-url.md) | replace the URL in the browser location bar |
+| [`hx-request`](@/attributes/hx-request.md) | configures various aspects of the request |
+| [`hx-sync`](@/attributes/hx-sync.md) | control how requests made by different elements are synchronized |
+| [`hx-validate`](@/attributes/hx-validate.md) | force elements to validate themselves before a request |
+| [`hx-vars`](@/attributes/hx-vars.md) | adds values dynamically to the parameters to submit with the request (deprecated, please use [`hx-vals`](@/attributes/hx-vals.md)) |
+
+
+
+## CSS Class Reference {#classes}
+
+
+
+| Class | Description |
+|-----------|-------------|
+| `htmx-added` | Applied to a new piece of content before it is swapped, removed after it is settled.
+| `htmx-indicator` | A dynamically generated class that will toggle visible (opacity:1) when a `htmx-request` class is present
+| `htmx-request` | Applied to either the element or the element specified with [`hx-indicator`](@/attributes/hx-indicator.md) while a request is ongoing
+| `htmx-settling` | Applied to a target after content is swapped, removed after it is settled. The duration can be modified via [`hx-swap`](@/attributes/hx-swap.md).
+| `htmx-swapping` | Applied to a target before any content is swapped, removed after it is swapped. The duration can be modified via [`hx-swap`](@/attributes/hx-swap.md).
+
+
+
+| Header | Description |
+|--------|-------------|
+| `HX-Boosted` | indicates that the request is via an element using [hx-boost](@/attributes/hx-boost.md)
+| `HX-Current-URL` | the current URL of the browser
+| `HX-History-Restore-Request` | "true" if the request is for history restoration after a miss in the local history cache
+| `HX-Prompt` | the user response to an [hx-prompt](@/attributes/hx-prompt.md)
+| `HX-Request` | always "true"
+| `HX-Target` | the `id` of the target element if it exists
+| `HX-Trigger-Name` | the `name` of the triggered element if it exists
+| `HX-Trigger` | the `id` of the triggered element if it exists
+
+
+
+| Header | Description |
+|------------------------------------------------------|-------------|
+| [`HX-Location`](@/headers/hx-location.md) | allows you to do a client-side redirect that does not do a full page reload
+| [`HX-Push-Url`](@/headers/hx-push-url.md) | pushes a new url into the history stack
+| [`HX-Redirect`](@/headers/hx-redirect.md) | can be used to do a client-side redirect to a new location
+| `HX-Refresh` | if set to "true" the client-side will do a full refresh of the page
+| [`HX-Replace-Url`](@/headers/hx-replace-url.md) | replaces the current URL in the location bar
+| `HX-Reswap` | allows you to specify how the response will be swapped. See [hx-swap](@/attributes/hx-swap.md) for possible values
+| `HX-Retarget` | a CSS selector that updates the target of the content update to a different element on the page
+| `HX-Reselect` | a CSS selector that allows you to choose which part of the response is used to be swapped in. Overrides an existing [`hx-select`](@/attributes/hx-select.md) on the triggering element
+| [`HX-Trigger`](@/headers/hx-trigger.md) | allows you to trigger client-side events
+| [`HX-Trigger-After-Settle`](@/headers/hx-trigger.md) | allows you to trigger client-side events after the settle step
+| [`HX-Trigger-After-Swap`](@/headers/hx-trigger.md) | allows you to trigger client-side events after the swap step
+
+
+
+## Event Reference {#events}
+
+
+
+| Event | Description |
+|-------|-------------|
+| [`htmx:abort`](@/events.md#htmx:abort) | send this event to an element to abort a request
+| [`htmx:afterOnLoad`](@/events.md#htmx:afterOnLoad) | triggered after an AJAX request has completed processing a successful response
+| [`htmx:afterProcessNode`](@/events.md#htmx:afterProcessNode) | triggered after htmx has initialized a node
+| [`htmx:afterRequest`](@/events.md#htmx:afterRequest) | triggered after an AJAX request has completed
+| [`htmx:afterSettle`](@/events.md#htmx:afterSettle) | triggered after the DOM has settled
+| [`htmx:afterSwap`](@/events.md#htmx:afterSwap) | triggered after new content has been swapped in
+| [`htmx:beforeCleanupElement`](@/events.md#htmx:beforeCleanupElement) | triggered before htmx [disables](@/attributes/hx-disable.md) an element or removes it from the DOM
+| [`htmx:beforeOnLoad`](@/events.md#htmx:beforeOnLoad) | triggered before any response processing occurs
+| [`htmx:beforeProcessNode`](@/events.md#htmx:beforeProcessNode) | triggered before htmx initializes a node
+| [`htmx:beforeRequest`](@/events.md#htmx:beforeRequest) | triggered before an AJAX request is made
+| [`htmx:beforeSwap`](@/events.md#htmx:beforeSwap) | triggered before a swap is done, allows you to configure the swap
+| [`htmx:beforeSend`](@/events.md#htmx:beforeSend) | triggered just before an ajax request is sent
+| [`htmx:configRequest`](@/events.md#htmx:configRequest) | triggered before the request, allows you to customize parameters, headers
+| [`htmx:confirm`](@/events.md#htmx:confirm) | triggered after a trigger occurs on an element, allows you to cancel (or delay) issuing the AJAX request
+| [`htmx:historyCacheError`](@/events.md#htmx:historyCacheError) | triggered on an error during cache writing
+| [`htmx:historyCacheMiss`](@/events.md#htmx:historyCacheMiss) | triggered on a cache miss in the history subsystem
+| [`htmx:historyCacheMissError`](@/events.md#htmx:historyCacheMissError) | triggered on a unsuccessful remote retrieval
+| [`htmx:historyCacheMissLoad`](@/events.md#htmx:historyCacheMissLoad) | triggered on a successful remote retrieval
+| [`htmx:historyRestore`](@/events.md#htmx:historyRestore) | triggered when htmx handles a history restoration action
+| [`htmx:beforeHistorySave`](@/events.md#htmx:beforeHistorySave) | triggered before content is saved to the history cache
+| [`htmx:load`](@/events.md#htmx:load) | triggered when new content is added to the DOM
+| [`htmx:noSSESourceError`](@/events.md#htmx:noSSESourceError) | triggered when an element refers to a SSE event in its trigger, but no parent SSE source has been defined
+| [`htmx:onLoadError`](@/events.md#htmx:onLoadError) | triggered when an exception occurs during the onLoad handling in htmx
+| [`htmx:oobAfterSwap`](@/events.md#htmx:oobAfterSwap) | triggered after an out of band element as been swapped in
+| [`htmx:oobBeforeSwap`](@/events.md#htmx:oobBeforeSwap) | triggered before an out of band element swap is done, allows you to configure the swap
+| [`htmx:oobErrorNoTarget`](@/events.md#htmx:oobErrorNoTarget) | triggered when an out of band element does not have a matching ID in the current DOM
+| [`htmx:prompt`](@/events.md#htmx:prompt) | triggered after a prompt is shown
+| [`htmx:pushedIntoHistory`](@/events.md#htmx:pushedIntoHistory) | triggered after an url is pushed into history
+| [`htmx:responseError`](@/events.md#htmx:responseError) | triggered when an HTTP response error (non-`200` or `300` response code) occurs
+| [`htmx:sendError`](@/events.md#htmx:sendError) | triggered when a network error prevents an HTTP request from happening
+| [`htmx:sseError`](@/events.md#htmx:sseError) | triggered when an error occurs with a SSE source
+| [`htmx:sseOpen`](/events#htmx:sseOpen) | triggered when a SSE source is opened
+| [`htmx:swapError`](@/events.md#htmx:swapError) | triggered when an error occurs during the swap phase
+| [`htmx:targetError`](@/events.md#htmx:targetError) | triggered when an invalid target is specified
+| [`htmx:timeout`](@/events.md#htmx:timeout) | triggered when a request timeout occurs
+| [`htmx:validation:validate`](@/events.md#htmx:validation:validate) | triggered before an element is validated
+| [`htmx:validation:failed`](@/events.md#htmx:validation:failed) | triggered when an element fails validation
+| [`htmx:validation:halted`](@/events.md#htmx:validation:halted) | triggered when a request is halted due to validation errors
+| [`htmx:xhr:abort`](@/events.md#htmx:xhr:abort) | triggered when an ajax request aborts
+| [`htmx:xhr:loadend`](@/events.md#htmx:xhr:loadend) | triggered when an ajax request ends
+| [`htmx:xhr:loadstart`](@/events.md#htmx:xhr:loadstart) | triggered when an ajax request starts
+| [`htmx:xhr:progress`](@/events.md#htmx:xhr:progress) | triggered periodically during an ajax request that supports progress events
+
+
+
+## JavaScript API Reference {#api}
+
+
+
+| Method | Description |
+|-------|-------------|
+| [`htmx.addClass()`](@/api.md#addClass) | Adds a class to the given element
+| [`htmx.ajax()`](@/api.md#ajax) | Issues an htmx-style ajax request
+| [`htmx.closest()`](@/api.md#closest) | Finds the closest parent to the given element matching the selector
+| [`htmx.config`](@/api.md#config) | A property that holds the current htmx config object
+| [`htmx.createEventSource`](@/api.md#createEventSource) | A property holding the function to create SSE EventSource objects for htmx
+| [`htmx.createWebSocket`](@/api.md#createWebSocket) | A property holding the function to create WebSocket objects for htmx
+| [`htmx.defineExtension()`](@/api.md#defineExtension) | Defines an htmx [extension](https://extensions.htmx.org)
+| [`htmx.find()`](@/api.md#find) | Finds a single element matching the selector
+| [`htmx.findAll()` `htmx.findAll(elt, selector)`](@/api.md#find) | Finds all elements matching a given selector
+| [`htmx.logAll()`](@/api.md#logAll) | Installs a logger that will log all htmx events
+| [`htmx.logger`](@/api.md#logger) | A property set to the current logger (default is `null`)
+| [`htmx.off()`](@/api.md#off) | Removes an event listener from the given element
+| [`htmx.on()`](@/api.md#on) | Creates an event listener on the given element, returning it
+| [`htmx.onLoad()`](@/api.md#onLoad) | Adds a callback handler for the `htmx:load` event
+| [`htmx.parseInterval()`](@/api.md#parseInterval) | Parses an interval declaration into a millisecond value
+| [`htmx.process()`](@/api.md#process) | Processes the given element and its children, hooking up any htmx behavior
+| [`htmx.remove()`](@/api.md#remove) | Removes the given element
+| [`htmx.removeClass()`](@/api.md#removeClass) | Removes a class from the given element
+| [`htmx.removeExtension()`](@/api.md#removeExtension) | Removes an htmx [extension](https://extensions.htmx.org)
+| [`htmx.swap()`](@/api.md#swap) | Performs swapping (and settling) of HTML content
+| [`htmx.takeClass()`](@/api.md#takeClass) | Takes a class from other elements for the given element
+| [`htmx.toggleClass()`](@/api.md#toggleClass) | Toggles a class from the given element
+| [`htmx.trigger()`](@/api.md#trigger) | Triggers an event on an element
+| [`htmx.values()`](@/api.md#values) | Returns the input values associated with the given element
+
+
+
+
+## Configuration Reference {#config}
+
+Htmx has some configuration options that can be accessed either programmatically or declaratively. They are
+listed below:
+
+
+
+| Config Variable | Info |
+|---------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `htmx.config.historyEnabled` | defaults to `true`, really only useful for testing |
+| `htmx.config.historyCacheSize` | defaults to 10 |
+| `htmx.config.refreshOnHistoryMiss` | defaults to `false`, if set to `true` htmx will issue a full page refresh on history misses rather than use an AJAX request |
+| `htmx.config.defaultSwapStyle` | defaults to `innerHTML` |
+| `htmx.config.defaultSwapDelay` | defaults to 0 |
+| `htmx.config.defaultSettleDelay` | defaults to 20 |
+| `htmx.config.includeIndicatorStyles` | defaults to `true` (determines if the indicator styles are loaded) |
+| `htmx.config.indicatorClass` | defaults to `htmx-indicator` |
+| `htmx.config.requestClass` | defaults to `htmx-request` |
+| `htmx.config.addedClass` | defaults to `htmx-added` |
+| `htmx.config.settlingClass` | defaults to `htmx-settling` |
+| `htmx.config.swappingClass` | defaults to `htmx-swapping` |
+| `htmx.config.allowEval` | defaults to `true`, can be used to disable htmx's use of eval for certain features (e.g. trigger filters) |
+| `htmx.config.allowScriptTags` | defaults to `true`, determines if htmx will process script tags found in new content |
+| `htmx.config.inlineScriptNonce` | defaults to `''`, meaning that no nonce will be added to inline scripts |
+| `htmx.config.inlineStyleNonce` | defaults to `''`, meaning that no nonce will be added to inline styles |
+| `htmx.config.attributesToSettle` | defaults to `["class", "style", "width", "height"]`, the attributes to settle during the settling phase |
+| `htmx.config.wsReconnectDelay` | defaults to `full-jitter` |
+| `htmx.config.wsBinaryType` | defaults to `blob`, the [the type of binary data](https://developer.mozilla.org/docs/Web/API/WebSocket/binaryType) being received over the WebSocket connection |
+| `htmx.config.disableSelector` | defaults to `[hx-disable], [data-hx-disable]`, htmx will not process elements with this attribute on it or a parent |
+| `htmx.config.withCredentials` | defaults to `false`, allow cross-site Access-Control requests using credentials such as cookies, authorization headers or TLS client certificates |
+| `htmx.config.timeout` | defaults to 0, the number of milliseconds a request can take before automatically being terminated |
+| `htmx.config.scrollBehavior` | defaults to 'instant', the scroll behavior when using the [show](@/attributes/hx-swap.md#scrolling-scroll-show) modifier with `hx-swap`. The allowed values are `instant` (scrolling should happen instantly in a single jump), `smooth` (scrolling should animate smoothly) and `auto` (scroll behavior is determined by the computed value of [scroll-behavior](https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-behavior)). |
+| `htmx.config.defaultFocusScroll` | if the focused element should be scrolled into view, defaults to false and can be overridden using the [focus-scroll](@/attributes/hx-swap.md#focus-scroll) swap modifier. |
+| `htmx.config.getCacheBusterParam` | defaults to false, if set to true htmx will append the target element to the `GET` request in the format `org.htmx.cache-buster=targetElementId` |
+| `htmx.config.globalViewTransitions` | if set to `true`, htmx will use the [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) API when swapping in new content. |
+| `htmx.config.methodsThatUseUrlParams` | defaults to `["get", "delete"]`, htmx will format requests with these methods by encoding their parameters in the URL, not the request body |
+| `htmx.config.selfRequestsOnly` | defaults to `true`, whether to only allow AJAX requests to the same domain as the current document |
+| `htmx.config.ignoreTitle` | defaults to `false`, if set to `true` htmx will not update the title of the document when a `title` tag is found in new content |
+| `htmx.config.scrollIntoViewOnBoost` | defaults to `true`, whether or not the target of a boosted element is scrolled into the viewport. If `hx-target` is omitted on a boosted element, the target defaults to `body`, causing the page to scroll to the top. |
+| `htmx.config.triggerSpecsCache` | defaults to `null`, the cache to store evaluated trigger specifications into, improving parsing performance at the cost of more memory usage. You may define a simple object to use a never-clearing cache, or implement your own system using a [proxy object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy) |
+| `htmx.config.responseHandling` | the default [Response Handling](@/docs.md#response-handling) behavior for response status codes can be configured here to either swap or error |
+| `htmx.config.allowNestedOobSwaps` | defaults to `true`, whether to process OOB swaps on elements that are nested within the main response element. See [Nested OOB Swaps](@/attributes/hx-swap-oob.md#nested-oob-swaps). |
+
+
+
+You can set them directly in javascript, or you can use a `meta` tag:
+
+```html
+
+```# 🗿 Surreal
+### Tiny jQuery alternative for plain Javascript with inline [Locality of Behavior](https://htmx.org/essays/locality-of-behaviour/)!
+
+![cover](https://user-images.githubusercontent.com/24665/171092805-b41286b2-be4a-4aab-9ee6-d604699cc507.png)
+(Art by [shahabalizadeh](https://www.deviantart.com/shahabalizadeh))
+
+
+## Why does this exist?
+
+For devs who love ergonomics! You may appreciate Surreal if:
+
+* You want to stay as close as possible to Vanilla JS.
+* Hate typing `document.querySelector` over.. and over..
+* Hate typing `addEventListener` over.. and over..
+* Really wish `document.querySelectorAll` had Array functions..
+* Really wish `this` would work in any inline `
+
+```
+
+See the [Live Example](https://gnat.github.io/surreal/example.html)! Then [view source](https://github.com/gnat/surreal/blob/main/example.html).
+
+## 🎁 Install
+
+Surreal is only 320 lines. No build step. No dependencies.
+
+[📥 Download](https://raw.githubusercontent.com/gnat/surreal/main/surreal.js) into your project, and add `` in your ``
+
+Or, 🌐 via CDN: ``
+
+## ⚡ Usage
+
+### 🔍️ DOM Selection
+
+* Select **one** element: `me(...)`
+ * Can be any of:
+ * CSS selector: `".button"`, `"#header"`, `"h1"`, `"body > .block"`
+ * Variables: `body`, `e`, `some_element`
+ * Events: `event.currentTarget` will be used.
+ * Surreal selectors: `me()`,`any()`
+ * Choose the start location in the DOM with the 2nd arg. (Default: `document`)
+ * 🔥 `any('button', me('#header')).classAdd('red')`
+ * Add `.red` to any `# 🌘 CSS Scope Inline
+
+![cover](https://github.com/gnat/css-scope-inline/assets/24665/c4935c1b-34e3-4220-9d42-11f064999a57)
+(Art by [shahabalizadeh](https://www.artstation.com/artwork/zDgdd))
+
+## Why does this exist?
+
+* You want an easy inline vanilla CSS experience without Tailwind CSS.
+* Hate creating unique class names over.. and over.. to use once.
+* You want to co-locate your styles for ⚡️ [Locality of Behavior (LoB)](https://htmx.org/essays/locality-of-behaviour/)
+* You wish `this` would work in `
+
+
+```
+See the [Live Example](https://gnat.github.io/css-scope-inline/example.html)! Then [view source](https://github.com/gnat/css-scope-inline/blob/main/example.html).
+
+## 🌘 How does it work?
+
+This uses `MutationObserver` to monitor the DOM, and the moment a `
+ red
+
green
+
green
+
green
+
yellow
+
blue
+
green
+
green
+
+
+
+ red
+
green
+
green
+
green
+
yellow
+
blue
+
green
+
green
+
+```
+
+### CSS variables and child elements
+At first glance, **Tailwind Example 2** looks very promising! Exciting ...but:
+* 🔴 **Every child style requires an explicit selector.**
+ * Tailwinds' shorthand advantages sadly disappear.
+ * Any more child styles added in Tailwind will become longer than vanilla CSS.
+ * This limited example is the best case scenario for Tailwind.
+* 🔴 Not visible on github: **no highlighting for properties and units** begins to be painful.
+```html
+
+
+
+
+
+
+
+
+
+
+
+
+```
+## 🔎 Technical FAQ
+* Why do you use `querySelectorAll()` and not just process the `MutationObserver` results directly?
+ * This was indeed the original design; it will work well up until you begin recieving subtrees (ex: DOM swaps with [htmx](https://htmx.org), ajax, jquery, etc.) which requires walking all subtree elements to ensure we do not miss a `
+
+
+```
+See the [Live Example](https://gnat.github.io/css-scope-inline/example.html)! Then [view source](https://github.com/gnat/css-scope-inline/blob/main/example.html).
+
+## 🌘 How does it work?
+
+This uses `MutationObserver` to monitor the DOM, and the moment a `
+ red
+
green
+
green
+
green
+
yellow
+
blue
+
green
+
green
+
+
+
+ red
+
green
+
green
+
green
+
yellow
+
blue
+
green
+
green
+
+```
+
+### CSS variables and child elements
+At first glance, **Tailwind Example 2** looks very promising! Exciting ...but:
+* 🔴 **Every child style requires an explicit selector.**
+ * Tailwinds' shorthand advantages sadly disappear.
+ * Any more child styles added in Tailwind will become longer than vanilla CSS.
+ * This limited example is the best case scenario for Tailwind.
+* 🔴 Not visible on github: **no highlighting for properties and units** begins to be painful.
+```html
+
+
+
+
+
+
+
+
+
+
+
+
+```
+## 🔎 Technical FAQ
+* Why do you use `querySelectorAll()` and not just process the `MutationObserver` results directly?
+ * This was indeed the original design; it will work well up until you begin recieving subtrees (ex: DOM swaps with [htmx](https://htmx.org), ajax, jquery, etc.) which requires walking all subtree elements to ensure we do not miss a `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
However, there are many situations where you need a custom ft component that creates a unique HTML tag (for example <zero-md></zero-md>). There are many options in FastHTML to do this, and this section will walk through them. Generally you want to use the highest level option that fits your needs.
+
+
+
+
+
+
+Real-world example
+
+
+
+
This external tutorial walks through a practical situation where you may want to create a custom HTML tag using a custom ft component. Seeing a real-world example is a good way to understand why the contents of this guide is useful.
+
+
+
+
NotStr
+
The first way is to use the NotStr class to use an HTML tag as a string. It works as a one-off but quickly becomes harder to work with as complexity grows. However we can see that you can genenrate the same xml using NotStr as the out-of-the-box components.
The next (and better) approach is to let FastHTML generate the component function for you. As you can see in our assert this creates a function that creates the HTML just as we wanted. This works even though there is not a Some_never_before_used_tag function in the fasthtml.components source code (you can verify this yourself by looking at the source code).
+
+
+
+
+
+
+Tip
+
+
+
+
Typically these tags are needed because a CSS or Javascript library created a new XML tag that isn’t default HTML. For example the zero-md javascript library looks for a <zero-md></zero-md> tag to know what to run its javascript code on. Most CSS libraries work by creating styling based on the class attribute, but they can also apply styling to an arbitrary HTML tag that they made up.
+
+
+
+
from fasthtml.components import Some_never_before_used_tag
+
+Some_never_before_used_tag()
The automatic creation isn’t magic. It’s just calling a python function __getattr__ and you can call it yourself to get the same result.
+
+
import fasthtml
+
+auto_called = fasthtml.components.Some_never_before_used_tag()
+manual_called = fasthtml.components.__getattr__('Some_never_before_used_tag')()
+
+# Proving they generate the same xml
+assert to_xml(auto_called) == to_xml(manual_called)
+
+
Knowing that, we know that it’s possible to create a different function that has different behavior than FastHTMLs default behavior by modifying how the ___getattr__ function creates the components! It’s only a few lines of code and reading that what it does is a great way to understand components more deeply.
+
+
+
+
+
+
+Tip
+
+
+
+
Dunder methods and functions are special functions that have double underscores at the beginning and end of their name. They are called at specific times in python so you can use them to cause customized behavior that makes sense for your specific use case. They can appear magical if you don’t know how python works, but they are extremely commonly used to modify python’s default behavior (__init__ is probably the most common one).
+
In a module __getattr__ is called to get an attribute. In fasthtml.components, this is defined to create components automatically for you.
+
+
+
For example if you want a component that creates <path></path> that doesn’t conflict names with pathlib.Path you can do that. FastHTML automatically creates new components with a 1:1 mapping and a consistent name, which is almost always what you want. But in some cases you may want to customize that and you can use the ft_hx function to do that differently than the default.
We can add any behavior in that function that we need to, so let’s go through some progressively complex examples that you may need in some of your projects.
+
+
Underscores in tags
+
Now that we understand how FastHTML generates components, we can create our own in all kinds of ways. For example, maybe we need a weird HTML tag that uses underscores. FastHTML replaces _ with - in tags because underscores in tags are highly unusual and rarely what you want, though it does come up rarely.
It also may be that an argument in an HTML tag uses characters that can’t be used in python arguments. To handle these you can define those args using a dictionary.
+
+
+
+
+
\ No newline at end of file
diff --git a/ref/defining_xt_component.md b/ref/defining_xt_component.md
new file mode 100644
index 00000000..d26dbb57
--- /dev/null
+++ b/ref/defining_xt_component.md
@@ -0,0 +1,202 @@
+# Custom Components
+
+
+
+
+The majority of the time the default [ft
+components](../explains/explaining_xt_components.html) are all you need
+(for example `Div`, `P`, `H1`, etc.).
+
+
+
+> **Pre-requisite Knowledge**
+>
+> If you don’t know what an ft component is, you should read [the
+> explaining ft components explainer
+> first](../explains/explaining_xt_components.html).
+
+
+
+However, there are many situations where you need a custom ft component
+that creates a unique HTML tag (for example ``).
+There are many options in FastHTML to do this, and this section will
+walk through them. Generally you want to use the highest level option
+that fits your needs.
+
+
+
+> **Real-world example**
+>
+> [This external
+> tutorial](https://isaac-flath.github.io/website/posts/boots/FasthtmlTutorial.html)
+> walks through a practical situation where you may want to create a
+> custom HTML tag using a custom ft component. Seeing a real-world
+> example is a good way to understand why the contents of this guide is
+> useful.
+
+
+
+## NotStr
+
+The first way is to use the `NotStr` class to use an HTML tag as a
+string. It works as a one-off but quickly becomes harder to work with as
+complexity grows. However we can see that you can genenrate the same xml
+using `NotStr` as the out-of-the-box components.
+
+``` python
+from fasthtml.common import NotStr,Div, to_xml
+```
+
+``` python
+div_NotStr = NotStr('')
+print(div_NotStr)
+```
+
+
+
+## Automatic Creation
+
+The next (and better) approach is to let FastHTML generate the component
+function for you. As you can see in our `assert` this creates a function
+that creates the HTML just as we wanted. This works even though there is
+not a `Some_never_before_used_tag` function in the `fasthtml.components`
+source code (you can verify this yourself by looking at the source
+code).
+
+
+
+> **Tip**
+>
+> Typically these tags are needed because a CSS or Javascript library
+> created a new XML tag that isn’t default HTML. For example the
+> `zero-md` javascript library looks for a `` tag to
+> know what to run its javascript code on. Most CSS libraries work by
+> creating styling based on the `class` attribute, but they can also
+> apply styling to an arbitrary HTML tag that they made up.
+
+
+
+``` python
+from fasthtml.components import Some_never_before_used_tag
+
+Some_never_before_used_tag()
+```
+
+``` html
+
+```
+
+## Manual Creation
+
+The automatic creation isn’t magic. It’s just calling a python function
+[`__getattr__`](https://AnswerDotAI.github.io/fasthtml/api/components.html#__getattr__)
+and you can call it yourself to get the same result.
+
+``` python
+import fasthtml
+
+auto_called = fasthtml.components.Some_never_before_used_tag()
+manual_called = fasthtml.components.__getattr__('Some_never_before_used_tag')()
+
+# Proving they generate the same xml
+assert to_xml(auto_called) == to_xml(manual_called)
+```
+
+Knowing that, we know that it’s possible to create a different function
+that has different behavior than FastHTMLs default behavior by modifying
+how the `___getattr__` function creates the components! It’s only a few
+lines of code and reading that what it does is a great way to understand
+components more deeply.
+
+
+
+> **Tip**
+>
+> Dunder methods and functions are special functions that have double
+> underscores at the beginning and end of their name. They are called at
+> specific times in python so you can use them to cause customized
+> behavior that makes sense for your specific use case. They can appear
+> magical if you don’t know how python works, but they are extremely
+> commonly used to modify python’s default behavior (`__init__` is
+> probably the most common one).
+>
+> In a module
+> [`__getattr__`](https://AnswerDotAI.github.io/fasthtml/api/components.html#__getattr__)
+> is called to get an attribute. In `fasthtml.components`, this is
+> defined to create components automatically for you.
+
+
+
+For example if you want a component that creates `` that
+doesn’t conflict names with `pathlib.Path` you can do that. FastHTML
+automatically creates new components with a 1:1 mapping and a consistent
+name, which is almost always what you want. But in some cases you may
+want to customize that and you can use the
+[`ft_hx`](https://AnswerDotAI.github.io/fasthtml/api/components.html#ft_hx)
+function to do that differently than the default.
+
+``` python
+from fasthtml.common import ft_hx
+
+def ft_path(*c, target_id=None, **kwargs):
+ return ft_hx('path', *c, target_id=target_id, **kwargs)
+
+ft_path()
+```
+
+``` html
+
+```
+
+We can add any behavior in that function that we need to, so let’s go
+through some progressively complex examples that you may need in some of
+your projects.
+
+### Underscores in tags
+
+Now that we understand how FastHTML generates components, we can create
+our own in all kinds of ways. For example, maybe we need a weird HTML
+tag that uses underscores. FastHTML replaces `_` with `-` in tags
+because underscores in tags are highly unusual and rarely what you want,
+though it does come up rarely.
+
+``` python
+def tag_with_underscores(*c, target_id=None, **kwargs):
+ return ft_hx('tag_with_underscores', *c, target_id=target_id, **kwargs)
+
+tag_with_underscores()
+```
+
+``` html
+
+```
+
+### Symbols (ie @) in tags
+
+Sometimes you may need to use a tag that uses characters that are not
+allowed in function names in python (again, very unusual).
+
+``` python
+def tag_with_AtSymbol(*c, target_id=None, **kwargs):
+ return ft_hx('tag-with-@symbol', *c, target_id=target_id, **kwargs)
+
+tag_with_AtSymbol()
+```
+
+``` html
+
+```
+
+### Symbols (ie @) in tag attributes
+
+It also may be that an argument in an HTML tag uses characters that
+can’t be used in python arguments. To handle these you can define those
+args using a dictionary.
+
+``` python
+Div(normal_arg='normal stuff',**{'notNormal:arg:with_varing@symbols!':'123'})
+```
+
+``` html
+
+```
diff --git a/ref/handlers.html b/ref/handlers.html
new file mode 100644
index 00000000..c4a086e7
--- /dev/null
+++ b/ref/handlers.html
@@ -0,0 +1,1609 @@
+
+
+
+
+
+
+
+
+
+
+Handling handlers – fasthtml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
The FastHTML class is the main application class for FastHTML apps.
+
+
rt = app.route
+
+
app.route is used to register route handlers. It is a decorator, which means we place it before a function that is used as a handler. Because it’s used frequently in most FastHTML applications, we often alias it as rt, as we do here.
+
+
Basic Route Handling
+
+
@rt("/hi")
+def get(): return'Hi there'
+
+
Handler functions can return strings directly. These strings are sent as the response body to the client.
+
+
cli = Client(app)
+
+
Client is a test client for FastHTML applications. It allows you to simulate requests to your app without running a server.
+
+
cli.get('/hi').text
+
+
'Hi there'
+
+
+
The get method on a Client instance simulates GET requests to the app. It returns a response object that has a .text attribute, which you can use to access the body of the response. It calls httpx.get internally – all httpx HTTP verbs are supported.
Handler functions can be defined for different HTTP methods on the same route. Here, we define a post handler for the /hi route. The Client instance can simulate different HTTP methods, including POST requests.
Handler functions can accept a req (or request) parameter, which represents the incoming request. This object contains information about the request, including headers. In this example, we return the host header from the request. The test client uses ‘testserver’ as the default host.
+
In this example, we use @app.get("/hostie") instead of @rt("/hostie"). The @app.get() decorator explicitly specifies the HTTP method (GET) for the route, while @rt() by default handles both GET and POST requests.
If the @rt decorator is used without arguments, it uses the function name as the route path. Here, the yoyo function becomes the handler for the /yoyo route. This handler responds to GET and POST methods, since a specific method wasn’t provided.
Handler functions can return FT objects, which are automatically converted to HTML strings. The FT class can take other FT components as arguments, such as Div. This allows for easy composition of HTML elements in your responses.
The rt decorator modifies the yoyo function by adding an rt() method. This method returns the route path associated with the handler. It’s a convenient way to reference the route of a handler function dynamically.
+
In the example, yoyo.rt() is used as the value for hx_post. This means when the div is clicked, it will trigger an HTMX POST request to the route of the yoyo handler. This approach allows for flexible, DRY code by avoiding hardcoded route strings and automatically updating if the route changes.
+
This pattern is particularly useful in larger applications where routes might change, or when building reusable components that need to reference their own routes dynamically.
The rt() method of handler functions can also accept parameters. When called with parameters, it returns the route path with a query string appended. In this example, show_host.rt(a='b') generates the path /hostie?a=b.
+
The Body component is used here to demonstrate nesting of FT components. Div is nested inside Body, showcasing how you can create more complex HTML structures.
+
The cls parameter is used to add a CSS class to the Div. This translates to the class attribute in the rendered HTML. (class can’t be used as a parameter name directly in Python since it’s a reserved word.)
Handler functions can return multiple FT objects as a tuple. The first item is treated as the Title, and the rest are added to the Body. When the request is not an HTMX request, FastHTML automatically adds necessary HTML boilerplate, including default head content with required scripts.
+
When using app.route (or rt), if the function name matches an HTTP verb (e.g., get, post, put, delete), that HTTP method is automatically used for the route. In this case, a path must be explicitly provided as an argument to the decorator.
For HTMX requests (indicated by the hx-request header), FastHTML returns only the specified components without the full HTML structure. This allows for efficient partial page updates in HTMX applications.
Handler functions can return a complete Html structure, including Head and Body components. When a full HTML structure is returned, FastHTML doesn’t add any additional boilerplate. This gives you full control over the HTML output when needed.
The index function is a special handler in FastHTML. When defined without arguments to the @rt decorator, it automatically becomes the handler for the root path ('/'). This is a convenient way to define the main page or entry point of your application.
+
+
+
Path and Query Parameters
+
+
@rt('/user/{nm}', name='gday')
+def get(nm:str=''): returnf"Good day to you, {nm}!"
+cli.get('/user/Alexis').text
+
+
'Good day to you, Alexis!'
+
+
+
Handler functions can use path parameters, defined using curly braces in the route – this is implemented by Starlette directly, so all Starlette path parameters can be used. These parameters are passed as arguments to the function.
+
The name parameter in the decorator allows you to give the route a name, which can be used for URL generation.
+
In this example, {nm} in the route becomes the nm parameter in the function. The function uses this parameter to create a personalized greeting.
The uri function is used to generate URLs for named routes. It takes the route name as its first argument, followed by any path or query parameters needed for that route.
+
In this example, uri('gday', nm='Alexis') generates the URL for the route named ‘gday’ (which we defined earlier as ‘/user/{nm}’), with ‘Alexis’ as the value for the ‘nm’ parameter.
+
The link parameter in FT components sets the href attribute of the rendered HTML element. By using uri(), we can dynamically generate correct URLs even if the underlying route structure changes.
+
This approach promotes maintainable code by centralizing route definitions and avoiding hardcoded URLs throughout the application.
The url_for method of the request object can be used to generate URLs for named routes. It takes the route name as its first argument, followed by any path parameters needed for that route.
+
In this example, req.url_for('gday', nm='Alexis') generates the full URL for the route named ‘gday’, including the scheme and host. Similarly, req.url_for('show_host') generates the URL for the ‘show_host’ route.
+
This method is particularly useful when you need to generate absolute URLs, such as for email links or API responses. It ensures that the correct host and scheme are included, even if the application is accessed through different domains or protocols.
+
+
app.url_path_for('gday', nm='Jeremy')
+
+
'/user/Jeremy'
+
+
+
The url_path_for method of the application can be used to generate URL paths for named routes. Unlike url_for, it returns only the path component of the URL, without the scheme or host.
+
In this example, app.url_path_for('gday', nm='Jeremy') generates the path ‘/user/Jeremy’ for the route named ‘gday’.
+
This method is useful when you need relative URLs or just the path component, such as for internal links or when constructing URLs in a host-agnostic manner.
/Users/jhoward/Documents/GitHub/fasthtml/fasthtml/core.py:175: UserWarning: `nope has no type annotation and is not a recognised special name, so is ignored.
+ if arg!='resp': warn(f"`{arg} has no type annotation and is not a recognised special name, so is ignored.")
+
+
+
''
+
+
+
Handler functions can include parameters, but they must be type-annotated or have special names (like req) to be recognized. In this example, the nope parameter is not annotated, so it’s ignored, resulting in a warning.
+
When a parameter is ignored, it doesn’t receive the value from the query string. This can lead to unexpected behavior, as the function attempts to return nope, which is undefined.
+
The cli.get('/oops?nope=1') call succeeds with a 200 OK status because the handler doesn’t raise an exception, but it returns an empty response, rather than the intended value.
+
To fix this, you should either add a type annotation to the parameter (e.g., def get(nope: str):) or use a recognized special name like req.
+
+
@rt('/html/{idx}')
+def get(idx:int): return Body(H4(f'Next is {idx+1}.'))
+print(cli.get('/html/1', **hxhdr).text)
+
+
<body>
+ <h4>Next is 2.</h4>
+ </body>
+
+
+
+
Path parameters can be type-annotated, and FastHTML will automatically convert them to the specified type if possible. In this example, idx is annotated as int, so it’s converted from the string in the URL to an integer.
The reg_re_param function is used to register custom path parameter types using regular expressions. Here, we define a new path parameter type called “imgext” that matches common image file extensions.
+
Handler functions can use complex path patterns with multiple parameters and custom types. In this example, the route pattern r'/static/{path:path}{fn}.{ext:imgext}' uses three path parameters:
+
+
path: A Starlette built-in type that matches any path segments
+
fn: The filename without extension
+
ext: Our custom “imgext” type that matches specific image extensions
We define ModelName as an enum with three possible values: “alexnet”, “resnet”, and “lenet”. Handler functions can use these enum types as parameter annotations. In this example, the nm parameter is annotated with ModelName, which ensures that only valid model names are accepted.
+
When a request is made with a valid model name, the handler function returns that name. This pattern is useful for creating type-safe APIs with a predefined set of valid values.
Handler functions can use Path objects as parameter types. The Path type is from Python’s standard library pathlib module, which provides an object-oriented interface for working with file paths. In this example, the path parameter is annotated with Path, so FastHTML automatically converts the string from the URL to a Path object.
+
This approach is particularly useful when working with file-related routes, as it provides a convenient and platform-independent way to handle file paths.
Handler functions can use query parameters, which are automatically parsed from the URL. In this example, idx is a query parameter with a default value of 0. It’s annotated as int|None, allowing it to be either an integer or None.
+
The function uses this parameter to index into a fake database (fake_db). When a request is made with a valid idx query parameter, the handler returns the corresponding item from the database.
+
+
print(cli.get('/items/').text)
+
+
{"name":"Foo"}
+
+
+
When no idx query parameter is provided, the handler function uses the default value of 0. This results in returning the first item from the fake_db list, which is {"name":"Foo"}.
+
This behavior demonstrates how default values for query parameters work in FastHTML. They allow the API to have a sensible default behavior when optional parameters are not provided.
+
+
print(cli.get('/items/?idx=g'))
+
+
<Response [404 Not Found]>
+
+
+
When an invalid value is provided for a typed query parameter, FastHTML returns a 404 Not Found response. In this example, ‘g’ is not a valid integer for the idx parameter, so the request fails with a 404 status.
+
This behavior ensures type safety and prevents invalid inputs from reaching the handler function.
Handler functions can use boolean query parameters. In this example, coming is a boolean parameter with a default value of True. FastHTML automatically converts string values like ‘true’, ‘false’, ‘1’, ‘0’, ‘on’, ‘off’, ‘yes’, and ‘no’ to their corresponding boolean values.
+
The underscore _ is used as the function name in this example to indicate that the function’s name is not important or won’t be referenced elsewhere. This is a common Python convention for throwaway or unused variables, and it works here because FastHTML uses the route decorator parameter, when provided, to determine the URL path, not the function name. By default, both get and post methods can be used in routes that don’t specify an http method (by either using app.get, def get, or the methods parameter to app.route).
+
+
@app.get("/datie/")
+def _(d:parsed_date): return d
+date_str ="17th of May, 2024, 2p"
+print(cli.get(f'/datie/?d={date_str}').text)
+
+
2024-05-17 14:00:00
+
+
+
Handler functions can use date objects as parameter types. FastHTML uses dateutil.parser library to automatically parse a wide variety of date string formats into date objects.
Handler functions can access HTTP headers by using parameter names that match the header names. In this example, user_agent is used as a parameter name, which automatically captures the value of the ‘User-Agent’ header from the request.
+
The Client instance allows setting custom headers for test requests. Here, we set the ‘User-Agent’ header to ‘FastHTML’ in the test request.
Handler functions can access HTMX-specific headers using either the special htmx parameter name, or a parameter annotated with HtmxHeaders. Both approaches provide access to HTMX-related information.
+
In these examples, the htmx.request attribute returns the value of the ‘HX-Request’ header.
Handler functions can access the FastHTML application instance using the special app parameter name. This allows handlers to access application-level attributes and methods.
+
In this example, we set a custom attribute chk on the application instance. The handler function then uses the app parameter to access this attribute and return its value.
Handler functions can access the FastHTML application instance using a parameter annotated with FastHTML. This allows handlers to access application-level attributes and methods, just like using the special app parameter name.
+
Handlers can return tuples containing both content and HttpHeader objects. HttpHeader allows setting custom HTTP headers in the response.
+
In this example:
+
+
We define a handler that returns both the chk attribute from the application and a custom header.
+
The HttpHeader("mykey", "myval") sets a custom header in the response.
+
We use the test client to make a request and examine both the response text and headers.
+
The response includes the custom header “mykey” along with standard headers like content-length and content-type.
Handler functions can return HtmxResponseHeaders objects to set HTMX-specific response headers. This is useful for HTMX-specific behaviors like client-side redirects.
+
In this example we define a handler that returns an HtmxResponseHeaders object with a location parameter, which sets the HX-Location header in the response. HTMX uses this for client-side redirects.
Handler functions can return Redirect objects to perform HTTP redirects. This is useful for redirecting users to different pages or external URLs.
+
In this example:
+
+
We define a handler that returns a Redirect object with the URL “http://example.org”.
+
The cli.get('/app4', follow_redirects=False) call simulates a GET request to the ‘/app4’ route without following redirects.
+
The response has a 303 See Other status code, indicating a redirect.
+
+
The follow_redirects=False parameter is used to prevent the test client from automatically following the redirect, allowing us to inspect the redirect response itself.
The Redirect class in FastHTML implements a __response__ method, which is a special method recognized by the framework. When a handler returns a Redirect object, FastHTML internally calls this __response__ method to replace the original response.
+
The __response__ method takes a req parameter, which represents the incoming request. This allows the method to access request information if needed when constructing the redirect response.
FastHTML automatically identifies elements typically placed in the <head> (like Title and Meta) and positions them accordingly, while other elements go in the <body>.
+
In this example: - (Title('hi'), H1('hi')) defines the title and main heading. The title is placed in the head, and the H1 in the body. - (Meta(property='image'), Meta(property='site_name')) defines two meta tags, which are both placed in the head.
Handler functions can accept form data parameters, without needing to manually extract it from the request. In this example, username is expected to be sent as form data.
+
If required form data is missing, FastHTML automatically returns a 400 Bad Request response with an error message.
+
The data parameter in the cli.post() method simulates sending form data in the request.
Handlers can have optional form data parameters with default values. In this example, dogname is an optional parameter with a default value of None.
+
Here, if the form data doesn’t include the dogname field, the function uses the default value. The function returns either the provided dogname or ‘unknown name’ if dogname is None.
You can use dataclasses to define structured form data. In this example, Bodie is a dataclass with a (int) and b (str) fields.
+
FastHTML automatically converts the incoming form data to a Bodie instance where attribute names match parameter names. Other form data elements are matched with parameters with the same names (in this case, nm).
+
Handler functions can return dictionaries, which FastHTML automatically JSON-encodes.
dict parameters capture all form data as a dictionary. In this example, the data parameter is annotated with dict, so FastHTML automatically converts all incoming form data into a dictionary.
+
Note that when form data is converted to a dictionary, all values become strings, even if they were originally numbers. This is why the ‘a’ key in the response has a string value “1” instead of the integer 1.
Handler functions can use named tuples to define structured form data. In this example, Bodient is a named tuple with a and b fields.
+
FastHTML automatically converts the incoming form data to a Bodient instance where field names match parameter names. As with the previous example, all form data values are converted to strings in the process.
+
+
class BodieTD(TypedDict): a:int;b:str='foo'
+
+@app.post("/bodietd/")
+def bodient(data:BodieTD): return data
+print(cli.post('/bodietd/', data=d).text)
+
+
{"a":1,"b":"foo"}
+
+
+
You can use TypedDict to define structured form data with type hints. In this example, BodieTD is a TypedDict with a (int) and b (str) fields, where b has a default value of ‘foo’.
+
FastHTML automatically converts the incoming form data to a BodieTD instance where keys match the defined fields. Unlike with regular dictionaries or named tuples, FastHTML respects the type hints in TypedDict, converting values to the specified types when possible (e.g., converting ‘1’ to the integer 1 for the ‘a’ field).
+
+
class Bodie2:
+ a:int|None; b:str
+def__init__(self, a, b='foo'): store_attr()
+
+@app.post("/bodie2/")
+def bodie(d:Bodie2): returnf"a: {d.a}; b: {d.b}"
+print(cli.post('/bodie2/', data={'a':1}).text)
+
+
a: 1; b: foo
+
+
+
Custom classes can be used to define structured form data. Here, Bodie2 is a custom class with a (int|None) and b (str) attributes, where b has a default value of ‘foo’. The store_attr() function (from fastcore) automatically assigns constructor parameters to instance attributes.
+
FastHTML automatically converts the incoming form data to a Bodie2 instance, matching form fields to constructor parameters. It respects type hints and default values.
Handler functions can accept JSON data as input, which is automatically parsed into the specified type. In this example, it is of type Bodie, and FastHTML converts the incoming JSON data to a Bodie instance.
+
The Titled component is used to create a page with a title and main content. It automatically generates an <h1> with the provided title, wraps the content in a <main> tag with a “container” class, and adds a title to the head.
+
When making a request with JSON data: - Set the “Content-Type” header to “application/json” - Provide the JSON data as a string in the data parameter of the request
+
+
+
Cookies, Sessions, File Uploads, and more
+
+
@rt("/setcookie")
+def get(): return cookie('now', datetime.now())
+
+@rt("/getcookie")
+def get(now:parsed_date): returnf'Cookie was set at time {now.time()}'
+
+print(cli.get('/setcookie').text)
+time.sleep(0.01)
+cli.get('/getcookie').text
+
+
+
+
+
'Cookie was set at time 16:53:08.485345'
+
+
+
Handler functions can set and retrieve cookies. In this example:
+
+
The /setcookie route sets a cookie named ‘now’ with the current datetime.
+
The /getcookie route retrieves the ‘now’ cookie and returns its value.
+
+
The cookie() function is used to create a cookie response. FastHTML automatically converts the datetime object to a string when setting the cookie, and parses it back to a date object when retrieving it.
The cookie() function returns an HttpHeader object with the ‘set-cookie’ key. You can return it in a tuple along with FT elements, along with anything else FastHTML supports in responses.
Sessions store and retrieve data across requests. To use sessions, you should to initialize the FastHTML application with a secret_key. This is used to cryptographically sign the cookie used by the session.
+
The sess parameter in handler functions provides access to the session data. You can set and get session variables using dictionary-style access.
Handler functions can accept file uploads using Starlette’s UploadFile type. In this example:
+
+
The /upload route accepts a file upload named uf.
+
The UploadFile object provides an asynchronous read() method to access the file contents.
+
We use await to read the file content asynchronously and decode it to a string.
+
+
We added async to the handler function because it uses await to read the file content asynchronously. In Python, any function that uses await must be declared as async. This allows the function to be run asynchronously, potentially improving performance by not blocking other operations while waiting for the file to be read.
The static_route method of the FastHTML application allows serving static files with specified extensions from a given directory. In this example:
+
+
.md files are served from the ../.. directory (two levels up from the current directory).
+
Accessing /README.md returns the contents of the README.md file from that directory.
+
+
+
help(app.static_route_exts)
+
+
Help on method static_route_exts in module fasthtml.core:
+
+static_route_exts(prefix='/', static_path='.', exts='static') method of fasthtml.core.FastHTML instance
+ Add a static route at URL path `prefix` with files from `static_path` and `exts` defined by `reg_re_param()`
+
FastHTML allows you to define custom exception handlers. In this example we defined a custom 404 (Not Found) handler function _not_found, which returns a Div component with the text ‘nope’.
+
FastHTML includes the full HTML structure, since we returned an HTML partial.
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ref/handlers.html.md b/ref/handlers.html.md
new file mode 100644
index 00000000..803acba6
--- /dev/null
+++ b/ref/handlers.html.md
@@ -0,0 +1,1131 @@
+# Handling handlers
+
+
+
+
+``` python
+from fasthtml.common import *
+from collections import namedtuple
+from typing import TypedDict
+from datetime import datetime
+import json,time
+```
+
+``` python
+app = FastHTML()
+```
+
+The
+[`FastHTML`](https://AnswerDotAI.github.io/fasthtml/api/core.html#fasthtml)
+class is the main application class for FastHTML apps.
+
+``` python
+rt = app.route
+```
+
+`app.route` is used to register route handlers. It is a decorator, which
+means we place it before a function that is used as a handler. Because
+it’s used frequently in most FastHTML applications, we often alias it as
+`rt`, as we do here.
+
+## Basic Route Handling
+
+``` python
+@rt("/hi")
+def get(): return 'Hi there'
+```
+
+Handler functions can return strings directly. These strings are sent as
+the response body to the client.
+
+``` python
+cli = Client(app)
+```
+
+[`Client`](https://AnswerDotAI.github.io/fasthtml/api/core.html#client)
+is a test client for FastHTML applications. It allows you to simulate
+requests to your app without running a server.
+
+``` python
+cli.get('/hi').text
+```
+
+ 'Hi there'
+
+The `get` method on a
+[`Client`](https://AnswerDotAI.github.io/fasthtml/api/core.html#client)
+instance simulates GET requests to the app. It returns a response object
+that has a `.text` attribute, which you can use to access the body of
+the response. It calls `httpx.get` internally – all httpx HTTP verbs are
+supported.
+
+``` python
+@rt("/hi")
+def post(): return 'Postal'
+cli.post('/hi').text
+```
+
+ 'Postal'
+
+Handler functions can be defined for different HTTP methods on the same
+route. Here, we define a `post` handler for the `/hi` route. The
+[`Client`](https://AnswerDotAI.github.io/fasthtml/api/core.html#client)
+instance can simulate different HTTP methods, including POST requests.
+
+## Request and Response Objects
+
+``` python
+@app.get("/hostie")
+def show_host(req): return req.headers['host']
+cli.get('/hostie').text
+```
+
+ 'testserver'
+
+Handler functions can accept a `req` (or `request`) parameter, which
+represents the incoming request. This object contains information about
+the request, including headers. In this example, we return the `host`
+header from the request. The test client uses ‘testserver’ as the
+default host.
+
+In this example, we use `@app.get("/hostie")` instead of
+`@rt("/hostie")`. The `@app.get()` decorator explicitly specifies the
+HTTP method (GET) for the route, while `@rt()` by default handles both
+GET and POST requests.
+
+``` python
+@rt
+def yoyo(): return 'a yoyo'
+cli.post('/yoyo').text
+```
+
+ 'a yoyo'
+
+If the `@rt` decorator is used without arguments, it uses the function
+name as the route path. Here, the `yoyo` function becomes the handler
+for the `/yoyo` route. This handler responds to GET and POST methods,
+since a specific method wasn’t provided.
+
+``` python
+@rt
+def ft1(): return Html(Div('Text.'))
+print(cli.get('/ft1').text)
+```
+
+
+
Text.
+
+
+Handler functions can return
+[`FT`](https://docs.fastht.ml/explains/explaining_xt_components.html)
+objects, which are automatically converted to HTML strings. The `FT`
+class can take other `FT` components as arguments, such as `Div`. This
+allows for easy composition of HTML elements in your responses.
+
+``` python
+@app.get
+def autopost(): return Html(Div('Text.', hx_post=yoyo.rt()))
+print(cli.get('/autopost').text)
+```
+
+
+
Text.
+
+
+The `rt` decorator modifies the `yoyo` function by adding an `rt()`
+method. This method returns the route path associated with the handler.
+It’s a convenient way to reference the route of a handler function
+dynamically.
+
+In the example, `yoyo.rt()` is used as the value for `hx_post`. This
+means when the div is clicked, it will trigger an HTMX POST request to
+the route of the `yoyo` handler. This approach allows for flexible, DRY
+code by avoiding hardcoded route strings and automatically updating if
+the route changes.
+
+This pattern is particularly useful in larger applications where routes
+might change, or when building reusable components that need to
+reference their own routes dynamically.
+
+``` python
+@app.get
+def autoget(): return Html(Body(Div('Text.', cls='px-2', hx_post=show_host.rt(a='b'))))
+print(cli.get('/autoget').text)
+```
+
+
+
+
Text.
+
+
+
+The `rt()` method of handler functions can also accept parameters. When
+called with parameters, it returns the route path with a query string
+appended. In this example, `show_host.rt(a='b')` generates the path
+`/hostie?a=b`.
+
+The `Body` component is used here to demonstrate nesting of FT
+components. `Div` is nested inside `Body`, showcasing how you can create
+more complex HTML structures.
+
+The `cls` parameter is used to add a CSS class to the `Div`. This
+translates to the `class` attribute in the rendered HTML. (`class` can’t
+be used as a parameter name directly in Python since it’s a reserved
+word.)
+
+``` python
+@rt('/ft2')
+def get(): return Title('Foo'),H1('bar')
+print(cli.get('/ft2').text)
+```
+
+
+
+
+
+ Foo
+
+
+
+
+
+
+
+
+
bar
+
+
+
+Handler functions can return multiple `FT` objects as a tuple. The first
+item is treated as the `Title`, and the rest are added to the `Body`.
+When the request is not an HTMX request, FastHTML automatically adds
+necessary HTML boilerplate, including default `head` content with
+required scripts.
+
+When using `app.route` (or `rt`), if the function name matches an HTTP
+verb (e.g., `get`, `post`, `put`, `delete`), that HTTP method is
+automatically used for the route. In this case, a path must be
+explicitly provided as an argument to the decorator.
+
+``` python
+hxhdr = {'headers':{'hx-request':"1"}}
+print(cli.get('/ft2', **hxhdr).text)
+```
+
+ Foo
+
+
bar
+
+For HTMX requests (indicated by the `hx-request` header), FastHTML
+returns only the specified components without the full HTML structure.
+This allows for efficient partial page updates in HTMX applications.
+
+``` python
+@rt('/ft3')
+def get(): return H1('bar')
+print(cli.get('/ft3', **hxhdr).text)
+```
+
+
bar
+
+When a handler function returns a single `FT` object for an HTMX
+request, it’s rendered as a single HTML partial.
+
+``` python
+@rt('/ft4')
+def get(): return Html(Head(Title('hi')), Body(P('there')))
+
+print(cli.get('/ft4').text)
+```
+
+
+
+ hi
+
+
+
there
+
+
+
+Handler functions can return a complete `Html` structure, including
+`Head` and `Body` components. When a full HTML structure is returned,
+FastHTML doesn’t add any additional boilerplate. This gives you full
+control over the HTML output when needed.
+
+``` python
+@rt
+def index(): return "welcome!"
+print(cli.get('/').text)
+```
+
+ welcome!
+
+The `index` function is a special handler in FastHTML. When defined
+without arguments to the `@rt` decorator, it automatically becomes the
+handler for the root path (`'/'`). This is a convenient way to define
+the main page or entry point of your application.
+
+## Path and Query Parameters
+
+``` python
+@rt('/user/{nm}', name='gday')
+def get(nm:str=''): return f"Good day to you, {nm}!"
+cli.get('/user/Alexis').text
+```
+
+ 'Good day to you, Alexis!'
+
+Handler functions can use path parameters, defined using curly braces in
+the route – this is implemented by Starlette directly, so all Starlette
+path parameters can be used. These parameters are passed as arguments to
+the function.
+
+The `name` parameter in the decorator allows you to give the route a
+name, which can be used for URL generation.
+
+In this example, `{nm}` in the route becomes the `nm` parameter in the
+function. The function uses this parameter to create a personalized
+greeting.
+
+``` python
+@app.get
+def autolink(): return Html(Div('Text.', link=uri('gday', nm='Alexis')))
+print(cli.get('/autolink').text)
+```
+
+
+
Text.
+
+
+The [`uri`](https://AnswerDotAI.github.io/fasthtml/api/core.html#uri)
+function is used to generate URLs for named routes. It takes the route
+name as its first argument, followed by any path or query parameters
+needed for that route.
+
+In this example, `uri('gday', nm='Alexis')` generates the URL for the
+route named ‘gday’ (which we defined earlier as ‘/user/{nm}’), with
+‘Alexis’ as the value for the ‘nm’ parameter.
+
+The `link` parameter in FT components sets the `href` attribute of the
+rendered HTML element. By using `uri()`, we can dynamically generate
+correct URLs even if the underlying route structure changes.
+
+This approach promotes maintainable code by centralizing route
+definitions and avoiding hardcoded URLs throughout the application.
+
+``` python
+@rt('/link')
+def get(req): return f"{req.url_for('gday', nm='Alexis')}; {req.url_for('show_host')}"
+
+cli.get('/link').text
+```
+
+ 'http://testserver/user/Alexis; http://testserver/hostie'
+
+The `url_for` method of the request object can be used to generate URLs
+for named routes. It takes the route name as its first argument,
+followed by any path parameters needed for that route.
+
+In this example, `req.url_for('gday', nm='Alexis')` generates the full
+URL for the route named ‘gday’, including the scheme and host.
+Similarly, `req.url_for('show_host')` generates the URL for the
+‘show_host’ route.
+
+This method is particularly useful when you need to generate absolute
+URLs, such as for email links or API responses. It ensures that the
+correct host and scheme are included, even if the application is
+accessed through different domains or protocols.
+
+``` python
+app.url_path_for('gday', nm='Jeremy')
+```
+
+ '/user/Jeremy'
+
+The `url_path_for` method of the application can be used to generate URL
+paths for named routes. Unlike `url_for`, it returns only the path
+component of the URL, without the scheme or host.
+
+In this example, `app.url_path_for('gday', nm='Jeremy')` generates the
+path ‘/user/Jeremy’ for the route named ‘gday’.
+
+This method is useful when you need relative URLs or just the path
+component, such as for internal links or when constructing URLs in a
+host-agnostic manner.
+
+``` python
+@rt('/oops')
+def get(nope): return nope
+r = cli.get('/oops?nope=1')
+print(r)
+r.text
+```
+
+
+
+ /Users/jhoward/Documents/GitHub/fasthtml/fasthtml/core.py:175: UserWarning: `nope has no type annotation and is not a recognised special name, so is ignored.
+ if arg!='resp': warn(f"`{arg} has no type annotation and is not a recognised special name, so is ignored.")
+
+ ''
+
+Handler functions can include parameters, but they must be
+type-annotated or have special names (like `req`) to be recognized. In
+this example, the `nope` parameter is not annotated, so it’s ignored,
+resulting in a warning.
+
+When a parameter is ignored, it doesn’t receive the value from the query
+string. This can lead to unexpected behavior, as the function attempts
+to return `nope`, which is undefined.
+
+The `cli.get('/oops?nope=1')` call succeeds with a 200 OK status because
+the handler doesn’t raise an exception, but it returns an empty
+response, rather than the intended value.
+
+To fix this, you should either add a type annotation to the parameter
+(e.g., `def get(nope: str):`) or use a recognized special name like
+`req`.
+
+``` python
+@rt('/html/{idx}')
+def get(idx:int): return Body(H4(f'Next is {idx+1}.'))
+print(cli.get('/html/1', **hxhdr).text)
+```
+
+
+
Next is 2.
+
+
+Path parameters can be type-annotated, and FastHTML will automatically
+convert them to the specified type if possible. In this example, `idx`
+is annotated as `int`, so it’s converted from the string in the URL to
+an integer.
+
+``` python
+reg_re_param("imgext", "ico|gif|jpg|jpeg|webm")
+
+@rt(r'/static/{path:path}{fn}.{ext:imgext}')
+def get(fn:str, path:str, ext:str): return f"Getting {fn}.{ext} from /{path}"
+
+print(cli.get('/static/foo/jph.ico').text)
+```
+
+ Getting jph.ico from /foo/
+
+The
+[`reg_re_param`](https://AnswerDotAI.github.io/fasthtml/api/core.html#reg_re_param)
+function is used to register custom path parameter types using regular
+expressions. Here, we define a new path parameter type called “imgext”
+that matches common image file extensions.
+
+Handler functions can use complex path patterns with multiple parameters
+and custom types. In this example, the route pattern
+`r'/static/{path:path}{fn}.{ext:imgext}'` uses three path parameters:
+
+1. `path`: A Starlette built-in type that matches any path segments
+2. `fn`: The filename without extension
+3. `ext`: Our custom “imgext” type that matches specific image
+ extensions
+
+``` python
+ModelName = str_enum('ModelName', "alexnet", "resnet", "lenet")
+
+@rt("/models/{nm}")
+def get(nm:ModelName): return nm
+
+print(cli.get('/models/alexnet').text)
+```
+
+ alexnet
+
+We define `ModelName` as an enum with three possible values: “alexnet”,
+“resnet”, and “lenet”. Handler functions can use these enum types as
+parameter annotations. In this example, the `nm` parameter is annotated
+with `ModelName`, which ensures that only valid model names are
+accepted.
+
+When a request is made with a valid model name, the handler function
+returns that name. This pattern is useful for creating type-safe APIs
+with a predefined set of valid values.
+
+``` python
+@rt("/files/{path}")
+async def get(path: Path): return path.with_suffix('.txt')
+print(cli.get('/files/foo').text)
+```
+
+ foo.txt
+
+Handler functions can use
+[`Path`](https://AnswerDotAI.github.io/fasthtml/api/svg.html#path)
+objects as parameter types. The
+[`Path`](https://AnswerDotAI.github.io/fasthtml/api/svg.html#path) type
+is from Python’s standard library `pathlib` module, which provides an
+object-oriented interface for working with file paths. In this example,
+the `path` parameter is annotated with
+[`Path`](https://AnswerDotAI.github.io/fasthtml/api/svg.html#path), so
+FastHTML automatically converts the string from the URL to a
+[`Path`](https://AnswerDotAI.github.io/fasthtml/api/svg.html#path)
+object.
+
+This approach is particularly useful when working with file-related
+routes, as it provides a convenient and platform-independent way to
+handle file paths.
+
+``` python
+fake_db = [{"name": "Foo"}, {"name": "Bar"}]
+
+@rt("/items/")
+def get(idx:int|None = 0): return fake_db[idx]
+print(cli.get('/items/?idx=1').text)
+```
+
+ {"name":"Bar"}
+
+Handler functions can use query parameters, which are automatically
+parsed from the URL. In this example, `idx` is a query parameter with a
+default value of 0. It’s annotated as `int|None`, allowing it to be
+either an integer or None.
+
+The function uses this parameter to index into a fake database
+(`fake_db`). When a request is made with a valid `idx` query parameter,
+the handler returns the corresponding item from the database.
+
+``` python
+print(cli.get('/items/').text)
+```
+
+ {"name":"Foo"}
+
+When no `idx` query parameter is provided, the handler function uses the
+default value of 0. This results in returning the first item from the
+`fake_db` list, which is `{"name":"Foo"}`.
+
+This behavior demonstrates how default values for query parameters work
+in FastHTML. They allow the API to have a sensible default behavior when
+optional parameters are not provided.
+
+``` python
+print(cli.get('/items/?idx=g'))
+```
+
+
+
+When an invalid value is provided for a typed query parameter, FastHTML
+returns a 404 Not Found response. In this example, ‘g’ is not a valid
+integer for the `idx` parameter, so the request fails with a 404 status.
+
+This behavior ensures type safety and prevents invalid inputs from
+reaching the handler function.
+
+``` python
+@app.get("/booly/")
+def _(coming:bool=True): return 'Coming' if coming else 'Not coming'
+print(cli.get('/booly/?coming=true').text)
+print(cli.get('/booly/?coming=no').text)
+```
+
+ Coming
+ Not coming
+
+Handler functions can use boolean query parameters. In this example,
+`coming` is a boolean parameter with a default value of `True`. FastHTML
+automatically converts string values like ‘true’, ‘false’, ‘1’, ‘0’,
+‘on’, ‘off’, ‘yes’, and ‘no’ to their corresponding boolean values.
+
+The underscore `_` is used as the function name in this example to
+indicate that the function’s name is not important or won’t be
+referenced elsewhere. This is a common Python convention for throwaway
+or unused variables, and it works here because FastHTML uses the route
+decorator parameter, when provided, to determine the URL path, not the
+function name. By default, both `get` and `post` methods can be used in
+routes that don’t specify an http method (by either using `app.get`,
+`def get`, or the `methods` parameter to `app.route`).
+
+``` python
+@app.get("/datie/")
+def _(d:parsed_date): return d
+date_str = "17th of May, 2024, 2p"
+print(cli.get(f'/datie/?d={date_str}').text)
+```
+
+ 2024-05-17 14:00:00
+
+Handler functions can use `date` objects as parameter types. FastHTML
+uses `dateutil.parser` library to automatically parse a wide variety of
+date string formats into `date` objects.
+
+``` python
+@app.get("/ua")
+async def _(user_agent:str): return user_agent
+print(cli.get('/ua', headers={'User-Agent':'FastHTML'}).text)
+```
+
+ FastHTML
+
+Handler functions can access HTTP headers by using parameter names that
+match the header names. In this example, `user_agent` is used as a
+parameter name, which automatically captures the value of the
+‘User-Agent’ header from the request.
+
+The
+[`Client`](https://AnswerDotAI.github.io/fasthtml/api/core.html#client)
+instance allows setting custom headers for test requests. Here, we set
+the ‘User-Agent’ header to ‘FastHTML’ in the test request.
+
+``` python
+@app.get("/hxtest")
+def _(htmx): return htmx.request
+print(cli.get('/hxtest', headers={'HX-Request':'1'}).text)
+
+@app.get("/hxtest2")
+def _(foo:HtmxHeaders, req): return foo.request
+print(cli.get('/hxtest2', headers={'HX-Request':'1'}).text)
+```
+
+ 1
+ 1
+
+Handler functions can access HTMX-specific headers using either the
+special `htmx` parameter name, or a parameter annotated with
+[`HtmxHeaders`](https://AnswerDotAI.github.io/fasthtml/api/core.html#htmxheaders).
+Both approaches provide access to HTMX-related information.
+
+In these examples, the `htmx.request` attribute returns the value of the
+‘HX-Request’ header.
+
+``` python
+app.chk = 'foo'
+@app.get("/app")
+def _(app): return app.chk
+print(cli.get('/app').text)
+```
+
+ foo
+
+Handler functions can access the
+[`FastHTML`](https://AnswerDotAI.github.io/fasthtml/api/core.html#fasthtml)
+application instance using the special `app` parameter name. This allows
+handlers to access application-level attributes and methods.
+
+In this example, we set a custom attribute `chk` on the application
+instance. The handler function then uses the `app` parameter to access
+this attribute and return its value.
+
+``` python
+@app.get("/app2")
+def _(foo:FastHTML): return foo.chk,HttpHeader("mykey", "myval")
+r = cli.get('/app2', **hxhdr)
+print(r.text)
+print(r.headers)
+```
+
+ foo
+
+ Headers({'mykey': 'myval', 'content-length': '4', 'content-type': 'text/html; charset=utf-8'})
+
+Handler functions can access the
+[`FastHTML`](https://AnswerDotAI.github.io/fasthtml/api/core.html#fasthtml)
+application instance using a parameter annotated with
+[`FastHTML`](https://AnswerDotAI.github.io/fasthtml/api/core.html#fasthtml).
+This allows handlers to access application-level attributes and methods,
+just like using the special `app` parameter name.
+
+Handlers can return tuples containing both content and
+[`HttpHeader`](https://AnswerDotAI.github.io/fasthtml/api/core.html#httpheader)
+objects.
+[`HttpHeader`](https://AnswerDotAI.github.io/fasthtml/api/core.html#httpheader)
+allows setting custom HTTP headers in the response.
+
+In this example:
+
+- We define a handler that returns both the `chk` attribute from the
+ application and a custom header.
+- The `HttpHeader("mykey", "myval")` sets a custom header in the
+ response.
+- We use the test client to make a request and examine both the response
+ text and headers.
+- The response includes the custom header “mykey” along with standard
+ headers like content-length and content-type.
+
+``` python
+@app.get("/app3")
+def _(foo:FastHTML): return HtmxResponseHeaders(location="http://example.org")
+r = cli.get('/app3')
+print(r.headers)
+```
+
+ Headers({'hx-location': 'http://example.org', 'content-length': '0', 'content-type': 'text/html; charset=utf-8'})
+
+Handler functions can return
+[`HtmxResponseHeaders`](https://AnswerDotAI.github.io/fasthtml/api/core.html#htmxresponseheaders)
+objects to set HTMX-specific response headers. This is useful for
+HTMX-specific behaviors like client-side redirects.
+
+In this example we define a handler that returns an
+[`HtmxResponseHeaders`](https://AnswerDotAI.github.io/fasthtml/api/core.html#htmxresponseheaders)
+object with a `location` parameter, which sets the `HX-Location` header
+in the response. HTMX uses this for client-side redirects.
+
+``` python
+@app.get("/app4")
+def _(foo:FastHTML): return Redirect("http://example.org")
+cli.get('/app4', follow_redirects=False)
+```
+
+
+
+Handler functions can return
+[`Redirect`](https://AnswerDotAI.github.io/fasthtml/api/core.html#redirect)
+objects to perform HTTP redirects. This is useful for redirecting users
+to different pages or external URLs.
+
+In this example:
+
+- We define a handler that returns a
+ [`Redirect`](https://AnswerDotAI.github.io/fasthtml/api/core.html#redirect)
+ object with the URL “http://example.org”.
+- The `cli.get('/app4', follow_redirects=False)` call simulates a GET
+ request to the ‘/app4’ route without following redirects.
+- The response has a 303 See Other status code, indicating a redirect.
+
+The `follow_redirects=False` parameter is used to prevent the test
+client from automatically following the redirect, allowing us to inspect
+the redirect response itself.
+
+``` python
+Redirect.__response__
+```
+
+
+
+The
+[`Redirect`](https://AnswerDotAI.github.io/fasthtml/api/core.html#redirect)
+class in FastHTML implements a `__response__` method, which is a special
+method recognized by the framework. When a handler returns a
+[`Redirect`](https://AnswerDotAI.github.io/fasthtml/api/core.html#redirect)
+object, FastHTML internally calls this `__response__` method to replace
+the original response.
+
+The `__response__` method takes a `req` parameter, which represents the
+incoming request. This allows the method to access request information
+if needed when constructing the redirect response.
+
+``` python
+@rt
+def meta():
+ return ((Title('hi'),H1('hi')),
+ (Meta(property='image'), Meta(property='site_name')))
+
+print(cli.post('/meta').text)
+```
+
+
+
+
+
+ hi
+
+
+
+
+
+
+
+
+
+
+
hi
+
+
+
+FastHTML automatically identifies elements typically placed in the
+`` (like `Title` and `Meta`) and positions them accordingly, while
+other elements go in the ``.
+
+In this example: - `(Title('hi'), H1('hi'))` defines the title and main
+heading. The title is placed in the head, and the H1 in the body. -
+`(Meta(property='image'), Meta(property='site_name'))` defines two meta
+tags, which are both placed in the head.
+
+## Form Data and JSON Handling
+
+``` python
+@app.post('/profile/me')
+def profile_update(username: str): return username
+
+print(cli.post('/profile/me', data={'username' : 'Alexis'}).text)
+r = cli.post('/profile/me', data={})
+print(r.text)
+r
+```
+
+ Alexis
+ Missing required field: username
+
+
+
+Handler functions can accept form data parameters, without needing to
+manually extract it from the request. In this example, `username` is
+expected to be sent as form data.
+
+If required form data is missing, FastHTML automatically returns a 400
+Bad Request response with an error message.
+
+The `data` parameter in the `cli.post()` method simulates sending form
+data in the request.
+
+``` python
+@app.post('/pet/dog')
+def pet_dog(dogname: str = None): return dogname or 'unknown name'
+print(cli.post('/pet/dog', data={}).text)
+```
+
+ unknown name
+
+Handlers can have optional form data parameters with default values. In
+this example, `dogname` is an optional parameter with a default value of
+`None`.
+
+Here, if the form data doesn’t include the `dogname` field, the function
+uses the default value. The function returns either the provided
+`dogname` or ‘unknown name’ if `dogname` is `None`.
+
+``` python
+@dataclass
+class Bodie: a:int;b:str
+
+@rt("/bodie/{nm}")
+def post(nm:str, data:Bodie):
+ res = asdict(data)
+ res['nm'] = nm
+ return res
+
+print(cli.post('/bodie/me', data=dict(a=1, b='foo', nm='me')).text)
+```
+
+ {"a":1,"b":"foo","nm":"me"}
+
+You can use dataclasses to define structured form data. In this example,
+`Bodie` is a dataclass with `a` (int) and `b` (str) fields.
+
+FastHTML automatically converts the incoming form data to a `Bodie`
+instance where attribute names match parameter names. Other form data
+elements are matched with parameters with the same names (in this case,
+`nm`).
+
+Handler functions can return dictionaries, which FastHTML automatically
+JSON-encodes.
+
+``` python
+@app.post("/bodied/")
+def bodied(data:dict): return data
+
+d = dict(a=1, b='foo')
+print(cli.post('/bodied/', data=d).text)
+```
+
+ {"a":"1","b":"foo"}
+
+`dict` parameters capture all form data as a dictionary. In this
+example, the `data` parameter is annotated with `dict`, so FastHTML
+automatically converts all incoming form data into a dictionary.
+
+Note that when form data is converted to a dictionary, all values become
+strings, even if they were originally numbers. This is why the ‘a’ key
+in the response has a string value “1” instead of the integer 1.
+
+``` python
+nt = namedtuple('Bodient', ['a','b'])
+
+@app.post("/bodient/")
+def bodient(data:nt): return asdict(data)
+print(cli.post('/bodient/', data=d).text)
+```
+
+ {"a":"1","b":"foo"}
+
+Handler functions can use named tuples to define structured form data.
+In this example, `Bodient` is a named tuple with `a` and `b` fields.
+
+FastHTML automatically converts the incoming form data to a `Bodient`
+instance where field names match parameter names. As with the previous
+example, all form data values are converted to strings in the process.
+
+``` python
+class BodieTD(TypedDict): a:int;b:str='foo'
+
+@app.post("/bodietd/")
+def bodient(data:BodieTD): return data
+print(cli.post('/bodietd/', data=d).text)
+```
+
+ {"a":1,"b":"foo"}
+
+You can use `TypedDict` to define structured form data with type hints.
+In this example, `BodieTD` is a `TypedDict` with `a` (int) and `b` (str)
+fields, where `b` has a default value of ‘foo’.
+
+FastHTML automatically converts the incoming form data to a `BodieTD`
+instance where keys match the defined fields. Unlike with regular
+dictionaries or named tuples, FastHTML respects the type hints in
+`TypedDict`, converting values to the specified types when possible
+(e.g., converting ‘1’ to the integer 1 for the ‘a’ field).
+
+``` python
+class Bodie2:
+ a:int|None; b:str
+ def __init__(self, a, b='foo'): store_attr()
+
+@app.post("/bodie2/")
+def bodie(d:Bodie2): return f"a: {d.a}; b: {d.b}"
+print(cli.post('/bodie2/', data={'a':1}).text)
+```
+
+ a: 1; b: foo
+
+Custom classes can be used to define structured form data. Here,
+`Bodie2` is a custom class with `a` (int|None) and `b` (str) attributes,
+where `b` has a default value of ‘foo’. The `store_attr()` function
+(from fastcore) automatically assigns constructor parameters to instance
+attributes.
+
+FastHTML automatically converts the incoming form data to a `Bodie2`
+instance, matching form fields to constructor parameters. It respects
+type hints and default values.
+
+``` python
+@app.post("/b")
+def index(it: Bodie): return Titled("It worked!", P(f"{it.a}, {it.b}"))
+
+s = json.dumps({"b": "Lorem", "a": 15})
+print(cli.post('/b', headers={"Content-Type": "application/json", 'hx-request':"1"}, data=s).text)
+```
+
+ It worked!
+
+
+
It worked!
+
15, Lorem
+
+
+Handler functions can accept JSON data as input, which is automatically
+parsed into the specified type. In this example, `it` is of type
+`Bodie`, and FastHTML converts the incoming JSON data to a `Bodie`
+instance.
+
+The
+[`Titled`](https://AnswerDotAI.github.io/fasthtml/api/xtend.html#titled)
+component is used to create a page with a title and main content. It
+automatically generates an `
` with the provided title, wraps the
+content in a `` tag with a “container” class, and adds a `title`
+to the head.
+
+When making a request with JSON data: - Set the “Content-Type” header to
+“application/json” - Provide the JSON data as a string in the `data`
+parameter of the request
+
+## Cookies, Sessions, File Uploads, and more
+
+``` python
+@rt("/setcookie")
+def get(): return cookie('now', datetime.now())
+
+@rt("/getcookie")
+def get(now:parsed_date): return f'Cookie was set at time {now.time()}'
+
+print(cli.get('/setcookie').text)
+time.sleep(0.01)
+cli.get('/getcookie').text
+```
+
+ 'Cookie was set at time 16:53:08.485345'
+
+Handler functions can set and retrieve cookies. In this example:
+
+- The `/setcookie` route sets a cookie named ‘now’ with the current
+ datetime.
+- The `/getcookie` route retrieves the ‘now’ cookie and returns its
+ value.
+
+The `cookie()` function is used to create a cookie response. FastHTML
+automatically converts the datetime object to a string when setting the
+cookie, and parses it back to a date object when retrieving it.
+
+``` python
+cookie('now', datetime.now())
+```
+
+ HttpHeader(k='set-cookie', v='now="2024-09-07 16:54:05.275757"; Path=/; SameSite=lax')
+
+The `cookie()` function returns an
+[`HttpHeader`](https://AnswerDotAI.github.io/fasthtml/api/core.html#httpheader)
+object with the ‘set-cookie’ key. You can return it in a tuple along
+with `FT` elements, along with anything else FastHTML supports in
+responses.
+
+``` python
+app = FastHTML(secret_key='soopersecret')
+cli = Client(app)
+rt = app.route
+```
+
+``` python
+@rt("/setsess")
+def get(sess, foo:str=''):
+ now = datetime.now()
+ sess['auth'] = str(now)
+ return f'Set to {now}'
+
+@rt("/getsess")
+def get(sess): return f'Session time: {sess["auth"]}'
+
+print(cli.get('/setsess').text)
+time.sleep(0.01)
+
+cli.get('/getsess').text
+```
+
+ Set to 2024-09-07 16:56:20.445950
+
+ 'Session time: 2024-09-07 16:56:20.445950'
+
+Sessions store and retrieve data across requests. To use sessions, you
+should to initialize the FastHTML application with a `secret_key`. This
+is used to cryptographically sign the cookie used by the session.
+
+The `sess` parameter in handler functions provides access to the session
+data. You can set and get session variables using dictionary-style
+access.
+
+``` python
+@rt("/upload")
+async def post(uf:UploadFile): return (await uf.read()).decode()
+
+with open('../../CHANGELOG.md', 'rb') as f:
+ print(cli.post('/upload', files={'uf':f}, data={'msg':'Hello'}).text[:15])
+```
+
+ # Release notes
+
+Handler functions can accept file uploads using Starlette’s `UploadFile`
+type. In this example:
+
+- The `/upload` route accepts a file upload named `uf`.
+- The `UploadFile` object provides an asynchronous `read()` method to
+ access the file contents.
+- We use `await` to read the file content asynchronously and decode it
+ to a string.
+
+We added `async` to the handler function because it uses `await` to read
+the file content asynchronously. In Python, any function that uses
+`await` must be declared as `async`. This allows the function to be run
+asynchronously, potentially improving performance by not blocking other
+operations while waiting for the file to be read.
+
+``` python
+app.static_route('.md', static_path='../..')
+print(cli.get('/README.md').text[:10])
+```
+
+ # FastHTML
+
+The `static_route` method of the FastHTML application allows serving
+static files with specified extensions from a given directory. In this
+example:
+
+- `.md` files are served from the `../..` directory (two levels up from
+ the current directory).
+- Accessing `/README.md` returns the contents of the README.md file from
+ that directory.
+
+``` python
+help(app.static_route_exts)
+```
+
+ Help on method static_route_exts in module fasthtml.core:
+
+ static_route_exts(prefix='/', static_path='.', exts='static') method of fasthtml.core.FastHTML instance
+ Add a static route at URL path `prefix` with files from `static_path` and `exts` defined by `reg_re_param()`
+
+``` python
+app.static_route_exts()
+print(cli.get('/README.txt').text[:50])
+```
+
+ These are the source notebooks for FastHTML.
+
+The `static_route_exts` method of the FastHTML application allows
+serving static files with specified extensions from a given directory.
+By default:
+
+- It serves files from the current directory (‘.’).
+- It uses the ‘static’ regex, which includes common static file
+ extensions like ‘ico’, ‘gif’, ‘jpg’, ‘css’, ‘js’, etc.
+- The URL prefix is set to ‘/’.
+
+The ‘static’ regex is defined by FastHTML using this code:
+
+``` python
+reg_re_param("static", "ico|gif|jpg|jpeg|webm|css|js|woff|png|svg|mp4|webp|ttf|otf|eot|woff2|txt|html|map")
+```
+
+``` python
+@rt("/form-submit/{list_id}")
+def options(list_id: str):
+ headers = {
+ 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Methods': 'POST',
+ 'Access-Control-Allow-Headers': '*',
+ }
+ return Response(status_code=200, headers=headers)
+
+print(cli.options('/form-submit/2').headers)
+```
+
+ Headers({'access-control-allow-origin': '*', 'access-control-allow-methods': 'POST', 'access-control-allow-headers': '*', 'content-length': '0', 'set-cookie': 'session_=eyJhdXRoIjogIjIwMjQtMDktMDcgMTY6NTY6MjAuNDQ1OTUwIiwgIm5hbWUiOiAiMiJ9.Ztv60A.N0b4z5rNg6GT3xCFc8X4DqwcNjQ; path=/; Max-Age=31536000; httponly; samesite=lax'})
+
+FastHTML handlers can handle OPTIONS requests and set custom headers. In
+this example:
+
+- The `/form-submit/{list_id}` route handles OPTIONS requests.
+- Custom headers are set to allow cross-origin requests (CORS).
+- The function returns a Starlette `Response` object with a 200 status
+ code and the custom headers.
+
+You can return any Starlette Response type from a handler function,
+giving you full control over the response when needed.
+
+``` python
+def _not_found(req, exc): return Div('nope')
+
+app = FastHTML(exception_handlers={404:_not_found})
+cli = Client(app)
+rt = app.route
+
+r = cli.get('/')
+print(r.text)
+```
+
+
+
+
+
+ FastHTML page
+
+
+
+
+
+
+
+
+
nope
+
+
+
+FastHTML allows you to define custom exception handlers. In this example
+we defined a custom 404 (Not Found) handler function `_not_found`, which
+returns a `Div` component with the text ‘nope’.
+
+FastHTML includes the full HTML structure, since we returned an HTML
+partial.
diff --git a/ref/live_reload.html b/ref/live_reload.html
new file mode 100644
index 00000000..cc620532
--- /dev/null
+++ b/ref/live_reload.html
@@ -0,0 +1,864 @@
+
+
+
+
+
+
+
+
+
+Live Reloading – fasthtml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
When building your app it can be useful to view your changes in a web browser as you make them. FastHTML supports live reloading which means that it watches for any changes to your code and automatically refreshes the webpage in your browser.
+
To enable live reloading simply replace FastHTML in your app with FastHTMLWithLiveReload.
+
from fasthtml.common import*
+app = FastHTMLWithLiveReload()
+
Then in your terminal run uvicorn with reloading enabled.
+
uvicorn main:app --reload
+
⚠️ Gotchas - A reload is only triggered when you save your changes. - FastHTMLWithLiveReload should only be used during development. - If your app spans multiple directories you might need to use the --reload-dir flag to watch all files in each directory. See the uvicorn docs for more info. - The live reload script is only injected into the page when rendering ft components.
In development the fast_app function provides the same functionality. It instantiates the FastHTMLWithLiveReload class if you pass live=True:
+
+
+
main.py
+
+
from fasthtml.common import*
+
+1app, rt = fast_app(live=True)
+
+2serve()
+
+
+
1
+
+fast_app() instantiates the FastHTMLWithLiveReload class.
+
+
2
+
+serve() is a wrapper around a uvicorn call.
+
+
+
To run main.py in live reload mode, just do python main.py. We recommend turning off live reload when deploying your app to production.
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ref/live_reload.html.md b/ref/live_reload.html.md
new file mode 100644
index 00000000..4e1b853b
--- /dev/null
+++ b/ref/live_reload.html.md
@@ -0,0 +1,61 @@
+# Live Reloading
+
+
+
+
+When building your app it can be useful to view your changes in a web
+browser as you make them. FastHTML supports live reloading which means
+that it watches for any changes to your code and automatically refreshes
+the webpage in your browser.
+
+To enable live reloading simply replace
+[`FastHTML`](https://AnswerDotAI.github.io/fasthtml/api/core.html#fasthtml)
+in your app with `FastHTMLWithLiveReload`.
+
+``` python
+from fasthtml.common import *
+app = FastHTMLWithLiveReload()
+```
+
+Then in your terminal run `uvicorn` with reloading enabled.
+
+ uvicorn main:app --reload
+
+**⚠️ Gotchas** - A reload is only triggered when you save your
+changes. - `FastHTMLWithLiveReload` should only be used during
+development. - If your app spans multiple directories you might need to
+use the `--reload-dir` flag to watch all files in each directory. See
+the uvicorn [docs](https://www.uvicorn.org/settings/#development) for
+more info. - The live reload script is only injected into the page when
+rendering [ft
+components](https://docs.fastht.ml/explains/explaining_xt_components.html).
+
+## Live reloading with [`fast_app`](https://AnswerDotAI.github.io/fasthtml/api/fastapp.html#fast_app)
+
+In development the
+[`fast_app`](https://AnswerDotAI.github.io/fasthtml/api/fastapp.html#fast_app)
+function provides the same functionality. It instantiates the
+`FastHTMLWithLiveReload` class if you pass `live=True`:
+
+
+
+Line 3
+`fast_app()` instantiates the `FastHTMLWithLiveReload` class.
+
+Line 5
+`serve()` is a wrapper around a `uvicorn` call.
+
+To run `main.py` in live reload mode, just do `python main.py`. We
+recommend turning off live reload when deploying your app to production.
diff --git a/robots.txt b/robots.txt
new file mode 100644
index 00000000..4edc0aac
--- /dev/null
+++ b/robots.txt
@@ -0,0 +1 @@
+Sitemap: https://AnswerDotAI.github.io/fasthtml/sitemap.xml
diff --git a/search.json b/search.json
new file mode 100644
index 00000000..b7ba0761
--- /dev/null
+++ b/search.json
@@ -0,0 +1,1433 @@
+[
+ {
+ "objectID": "ref/live_reload.html",
+ "href": "ref/live_reload.html",
+ "title": "Live Reloading",
+ "section": "",
+ "text": "When building your app it can be useful to view your changes in a web browser as you make them. FastHTML supports live reloading which means that it watches for any changes to your code and automatically refreshes the webpage in your browser.\nTo enable live reloading simply replace FastHTML in your app with FastHTMLWithLiveReload.\nThen in your terminal run uvicorn with reloading enabled.\n⚠️ Gotchas - A reload is only triggered when you save your changes. - FastHTMLWithLiveReload should only be used during development. - If your app spans multiple directories you might need to use the --reload-dir flag to watch all files in each directory. See the uvicorn docs for more info. - The live reload script is only injected into the page when rendering ft components.",
+ "crumbs": [
+ "Home",
+ "Reference",
+ "Live Reloading"
+ ]
+ },
+ {
+ "objectID": "ref/live_reload.html#live-reloading-with-fast_app",
+ "href": "ref/live_reload.html#live-reloading-with-fast_app",
+ "title": "Live Reloading",
+ "section": "Live reloading with fast_app",
+ "text": "Live reloading with fast_app\nIn development the fast_app function provides the same functionality. It instantiates the FastHTMLWithLiveReload class if you pass live=True:\n\n\nmain.py\n\nfrom fasthtml.common import *\n\n1app, rt = fast_app(live=True)\n\n2serve()\n\n\n1\n\nfast_app() instantiates the FastHTMLWithLiveReload class.\n\n2\n\nserve() is a wrapper around a uvicorn call.\n\n\nTo run main.py in live reload mode, just do python main.py. We recommend turning off live reload when deploying your app to production.",
+ "crumbs": [
+ "Home",
+ "Reference",
+ "Live Reloading"
+ ]
+ },
+ {
+ "objectID": "ref/defining_xt_component.html",
+ "href": "ref/defining_xt_component.html",
+ "title": "Custom Components",
+ "section": "",
+ "text": "The majority of the time the default ft components are all you need (for example Div, P, H1, etc.).\nHowever, there are many situations where you need a custom ft component that creates a unique HTML tag (for example <zero-md></zero-md>). There are many options in FastHTML to do this, and this section will walk through them. Generally you want to use the highest level option that fits your needs.",
+ "crumbs": [
+ "Home",
+ "Reference",
+ "Custom Components"
+ ]
+ },
+ {
+ "objectID": "ref/defining_xt_component.html#notstr",
+ "href": "ref/defining_xt_component.html#notstr",
+ "title": "Custom Components",
+ "section": "NotStr",
+ "text": "NotStr\nThe first way is to use the NotStr class to use an HTML tag as a string. It works as a one-off but quickly becomes harder to work with as complexity grows. However we can see that you can genenrate the same xml using NotStr as the out-of-the-box components.\n\nfrom fasthtml.common import NotStr,Div, to_xml\n\n\ndiv_NotStr = NotStr('<div></div>') \nprint(div_NotStr)\n\n<div></div>",
+ "crumbs": [
+ "Home",
+ "Reference",
+ "Custom Components"
+ ]
+ },
+ {
+ "objectID": "ref/defining_xt_component.html#automatic-creation",
+ "href": "ref/defining_xt_component.html#automatic-creation",
+ "title": "Custom Components",
+ "section": "Automatic Creation",
+ "text": "Automatic Creation\nThe next (and better) approach is to let FastHTML generate the component function for you. As you can see in our assert this creates a function that creates the HTML just as we wanted. This works even though there is not a Some_never_before_used_tag function in the fasthtml.components source code (you can verify this yourself by looking at the source code).\n\n\n\n\n\n\nTip\n\n\n\nTypically these tags are needed because a CSS or Javascript library created a new XML tag that isn’t default HTML. For example the zero-md javascript library looks for a <zero-md></zero-md> tag to know what to run its javascript code on. Most CSS libraries work by creating styling based on the class attribute, but they can also apply styling to an arbitrary HTML tag that they made up.\n\n\n\nfrom fasthtml.components import Some_never_before_used_tag\n\nSome_never_before_used_tag()\n\n<some-never-before-used-tag></some-never-before-used-tag>",
+ "crumbs": [
+ "Home",
+ "Reference",
+ "Custom Components"
+ ]
+ },
+ {
+ "objectID": "ref/defining_xt_component.html#manual-creation",
+ "href": "ref/defining_xt_component.html#manual-creation",
+ "title": "Custom Components",
+ "section": "Manual Creation",
+ "text": "Manual Creation\nThe automatic creation isn’t magic. It’s just calling a python function __getattr__ and you can call it yourself to get the same result.\n\nimport fasthtml\n\nauto_called = fasthtml.components.Some_never_before_used_tag()\nmanual_called = fasthtml.components.__getattr__('Some_never_before_used_tag')()\n\n# Proving they generate the same xml\nassert to_xml(auto_called) == to_xml(manual_called)\n\nKnowing that, we know that it’s possible to create a different function that has different behavior than FastHTMLs default behavior by modifying how the ___getattr__ function creates the components! It’s only a few lines of code and reading that what it does is a great way to understand components more deeply.\n\n\n\n\n\n\nTip\n\n\n\nDunder methods and functions are special functions that have double underscores at the beginning and end of their name. They are called at specific times in python so you can use them to cause customized behavior that makes sense for your specific use case. They can appear magical if you don’t know how python works, but they are extremely commonly used to modify python’s default behavior (__init__ is probably the most common one).\nIn a module __getattr__ is called to get an attribute. In fasthtml.components, this is defined to create components automatically for you.\n\n\nFor example if you want a component that creates <path></path> that doesn’t conflict names with pathlib.Path you can do that. FastHTML automatically creates new components with a 1:1 mapping and a consistent name, which is almost always what you want. But in some cases you may want to customize that and you can use the ft_hx function to do that differently than the default.\n\nfrom fasthtml.common import ft_hx\n\ndef ft_path(*c, target_id=None, **kwargs): \n return ft_hx('path', *c, target_id=target_id, **kwargs)\n\nft_path()\n\n<path></path>\n\n\nWe can add any behavior in that function that we need to, so let’s go through some progressively complex examples that you may need in some of your projects.\n\nUnderscores in tags\nNow that we understand how FastHTML generates components, we can create our own in all kinds of ways. For example, maybe we need a weird HTML tag that uses underscores. FastHTML replaces _ with - in tags because underscores in tags are highly unusual and rarely what you want, though it does come up rarely.\n\ndef tag_with_underscores(*c, target_id=None, **kwargs): \n return ft_hx('tag_with_underscores', *c, target_id=target_id, **kwargs)\n\ntag_with_underscores()\n\n<tag_with_underscores></tag_with_underscores>\n\n\n\n\nSymbols (ie @) in tags\nSometimes you may need to use a tag that uses characters that are not allowed in function names in python (again, very unusual).\n\ndef tag_with_AtSymbol(*c, target_id=None, **kwargs): \n return ft_hx('tag-with-@symbol', *c, target_id=target_id, **kwargs)\n\ntag_with_AtSymbol()\n\n<tag-with-@symbol></tag-with-@symbol>\n\n\n\n\nSymbols (ie @) in tag attributes\nIt also may be that an argument in an HTML tag uses characters that can’t be used in python arguments. To handle these you can define those args using a dictionary.\n\nDiv(normal_arg='normal stuff',**{'notNormal:arg:with_varing@symbols!':'123'})\n\n<div normal-arg=\"normal stuff\" notnormal:arg:with_varing@symbols!=\"123\"></div>",
+ "crumbs": [
+ "Home",
+ "Reference",
+ "Custom Components"
+ ]
+ },
+ {
+ "objectID": "explains/minidataapi.html",
+ "href": "explains/minidataapi.html",
+ "title": "MiniDataAPI Spec",
+ "section": "",
+ "text": "The MiniDataAPI is a persistence API specification that designed to be small and relatively easy to implement across a wide range of datastores. While early implementations have been SQL-based, the specification can be quickly implemented in key/value stores, document databases, and more.",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "MiniDataAPI Spec"
+ ]
+ },
+ {
+ "objectID": "explains/minidataapi.html#why",
+ "href": "explains/minidataapi.html#why",
+ "title": "MiniDataAPI Spec",
+ "section": "Why?",
+ "text": "Why?\nThe MiniDataAPI specification allows us to use the same API for many different database engines. Any application using the MiniDataAPI spec for interacting with its database requires no modification beyond import and configuration changes to switch database engines. For example, to convert an application from Fastlite running SQLite to FastSQL running PostgreSQL, should require only changing these two lines:\n\n\nFastLite version\nfrom fastlite import *\ndb = Database('test.db')\n\nFastSQL version\nfrom fastsql import *\ndb = Database('postgres:...')\n\n\nAs both libraries adhere to the MiniDataAPI specification, the rest of the code in the application should remain the same. The advantage of the MiniDataAPI spec is that it allows people to use whatever datastores they have access to or prefer.\n\n\n\n\n\n\nNote\n\n\n\nSwitching databases won’t migrate any existing data between databases.\n\n\n\nEasy to learn, quick to implement\nThe MiniDataAPI specification is designed to be easy-to-learn and quick to implement. It focuses on straightforward Create, Read, Update, and Delete (CRUD) operations.\nMiniDataAPI databases aren’t limited to just row-based systems. In fact, the specification is closer in design to a key/value store than a set of records. What’s exciting about this is we can write implementations for tools like Python dict stored as JSON, Redis, and even the venerable ZODB.\n\n\nLimitations of the MiniDataAPI Specification\n\n“Mini refers to the lightweightness of specification, not the data.”\n– Jeremy Howard\n\nThe advantages of the MiniDataAPI come at a cost. The MiniDataAPI specification focuses a very small set of features compared to what can be found in full-fledged ORMs and query languages. It intentionally avoids nuances or sophisticated features.\nThis means the specification does not include joins or formal foreign keys. Complex data stored over multiple tables that require joins isn’t handled well. For this kind of scenario it’s probably for the best to use more sophisticated ORMs or even direct database queries.\n\n\nSummary of the MiniDataAPI Design\n\nEasy-to-learn\nRelative quick to implement for new database engines\nAn API for CRUD operations\nFor many different types of databases including row- and key/value-based designs\nIntentionally small in terms of features: no joins, no foreign keys, no database specific features\nBest for simpler designs, complex architectures will need more sophisticated tools.",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "MiniDataAPI Spec"
+ ]
+ },
+ {
+ "objectID": "explains/minidataapi.html#connectconstruct-the-database",
+ "href": "explains/minidataapi.html#connectconstruct-the-database",
+ "title": "MiniDataAPI Spec",
+ "section": "Connect/construct the database",
+ "text": "Connect/construct the database\nWe connect or construct the database by passing in a string connecting to the database endpoint or a filepath representing the database’s location. While this example is for SQLite running in memory, other databases such as PostgreSQL, Redis, MongoDB, might instead use a URI pointing at the database’s filepath or endpoint. The method of connecting to a DB is not part of this API, but part of the underlying library. For instance, for fastlite:\n\ndb = database(':memory:')\n\nHere’s a complete list of the available methods in the API, all documented below (assuming db is a database and t is a table):\n\ndb.create\nt.insert\nt.delete\nt.update\nt[key]\nt(...)\nt.xtra",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "MiniDataAPI Spec"
+ ]
+ },
+ {
+ "objectID": "explains/minidataapi.html#tables",
+ "href": "explains/minidataapi.html#tables",
+ "title": "MiniDataAPI Spec",
+ "section": "Tables",
+ "text": "Tables\nFor the sake of expediency, this document uses a SQL example. However, tables can represent anything, not just the fundamental construct of a SQL databases. They might represent keys within a key/value structure or files on a hard-drive.\n\nCreating tables\nWe use a create() method attached to Database object (db in our example) to create the tables.\n\nclass User: name:str; email: str; year_started:int\nusers = db.create(User, pk='name')\nusers\n\n<Table user (name, email, year_started)>\n\n\n\nclass User: name:str; email: str; year_started:int\nusers = db.create(User, pk='name')\nusers\n\n<Table user (name, email, year_started)>\n\n\nIf no pk is provided, id is assumed to be the primary key. Regardless of whether you mark a class as a dataclass or not, it will be turned into one – specifically into a flexiclass.\n\n@dataclass\nclass Todo: id: int; title: str; detail: str; status: str; name: str\ntodos = db.create(Todo) \ntodos\n\n<Table todo (id, title, detail, status, name)>\n\n\n\n\nCompound primary keys\nThe MiniData API spec supports compound primary keys, where more than one column is used to identify records. We’ll also use this example to demonstrate creating a table using a dict of keyword arguments.\n\nclass Publication: authors: str; year: int; title: str\npublications = db.create(Publication, pk=('authors', 'year'))\n\n\n\nTransforming tables\nDepending on the database type, this method can include transforms - the ability to modify the tables. Let’s go ahead and add a password field for our table called pwd.\n\nclass User: name:str; email: str; year_started:int; pwd:str\nusers = db.create(User, pk='name', transform=True)\nusers\n\n<Table user (name, email, year_started, pwd)>",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "MiniDataAPI Spec"
+ ]
+ },
+ {
+ "objectID": "explains/minidataapi.html#manipulating-data",
+ "href": "explains/minidataapi.html#manipulating-data",
+ "title": "MiniDataAPI Spec",
+ "section": "Manipulating data",
+ "text": "Manipulating data\nThe specification is designed to provide as straightforward CRUD API (Create, Read, Update, and Delete) as possible. Additional features like joins are out of scope.\n\n.insert()\nAdd a new record to the database. We want to support as many types as possible, for now we have tests for Python classes, dataclasses, and dicts. Returns an instance of the new record.\nHere’s how to add a record using a Python class:\n\nusers.insert(User(name='Braden', email='b@example.com', year_started=2018))\n\nUser(name='Braden', email='b@example.com', year_started=2018, pwd=None)\n\n\nWe can also use keyword arguments directly:\n\nusers.insert(name='Alma', email='a@example.com', year_started=2019)\n\nUser(name='Alma', email='a@example.com', year_started=2019, pwd=None)\n\n\nAnd now Charlie gets added via a Python dict.\n\nusers.insert({'name': 'Charlie', 'email': 'c@example.com', 'year_started': 2018})\n\nUser(name='Charlie', email='c@example.com', year_started=2018, pwd=None)\n\n\nAnd now TODOs. Note that the inserted row is returned:\n\ntodos.insert(Todo(title='Write MiniDataAPI spec', status='open', name='Braden'))\ntodos.insert(title='Implement SSE in FastHTML', status='open', name='Alma')\ntodo = todos.insert(dict(title='Finish development of FastHTML', status='closed', name='Charlie'))\ntodo\n\nTodo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')\n\n\nLet’s do the same with the Publications table.\n\npublications.insert(Publication(authors='Alma', year=2019, title='FastHTML'))\npublications.insert(authors='Alma', year=2030, title='FastHTML and beyond')\npublication= publications.insert((dict(authors='Alma', year=2035, title='FastHTML, the early years')))\npublication\n\nPublication(authors='Alma', year=2035, title='FastHTML, the early years')\n\n\n\n\nSquare bracket search []\nGet a single record by entering a primary key into a table object within square brackets. Let’s see if we can find Alma.\n\nuser = users['Alma']\nuser\n\nUser(name='Alma', email='a@example.com', year_started=2019, pwd=None)\n\n\nIf no record is found, a NotFoundError error is raised. Here we look for David, who hasn’t yet been added to our users table.\n\ntry: users['David']\nexcept NotFoundError: print(f'User not found')\n\nUser not found\n\n\nHere’s a demonstration of a ticket search, demonstrating how this works with non-string primary keys.\n\ntodos[1]\n\nTodo(id=1, title='Write MiniDataAPI spec', detail=None, status='open', name='Braden')\n\n\nCompound primary keys can be supplied in lists or tuples, in the order they were defined. In this case it is the authors and year columns.\nHere’s a query by compound primary key done with a list:\n\npublications[['Alma', 2019]]\n\nPublication(authors='Alma', year=2019, title='FastHTML')\n\n\nHere’s the same query done directly with index args.\n\npublications['Alma', 2030]\n\nPublication(authors='Alma', year=2030, title='FastHTML and beyond')\n\n\n\n\nParentheses search ()\nGet zero to many records by entering values with parentheses searches. If nothing is in the parentheses, then everything is returned.\n\nusers()\n\n[User(name='Braden', email='b@example.com', year_started=2018, pwd=None),\n User(name='Alma', email='a@example.com', year_started=2019, pwd=None),\n User(name='Charlie', email='c@example.com', year_started=2018, pwd=None)]\n\n\nWe can order the results.\n\nusers(order_by='name')\n\n[User(name='Alma', email='a@example.com', year_started=2019, pwd=None),\n User(name='Braden', email='b@example.com', year_started=2018, pwd=None),\n User(name='Charlie', email='c@example.com', year_started=2018, pwd=None)]\n\n\nWe can filter on the results:\n\nusers(where=\"name='Alma'\")\n\n[User(name='Alma', email='a@example.com', year_started=2019, pwd=None)]\n\n\nGenerally you probably want to use placeholders, to avoid SQL injection attacks:\n\nusers(\"name=?\", ('Alma',))\n\n[User(name='Alma', email='a@example.com', year_started=2019, pwd=None)]\n\n\nWe can limit results with the limit keyword:\n\nusers(limit=1)\n\n[User(name='Braden', email='b@example.com', year_started=2018, pwd=None)]\n\n\nIf we’re using the limit keyword, we can also use the offset keyword to start the query later.\n\nusers(limit=5, offset=1)\n\n[User(name='Alma', email='a@example.com', year_started=2019, pwd=None),\n User(name='Charlie', email='c@example.com', year_started=2018, pwd=None)]\n\n\n\n\n.update()\nUpdate an existing record of the database. Must accept Python dict, dataclasses, and standard classes. Uses the primary key for identifying the record to be changed. Returns an instance of the updated record.\nHere’s with a normal Python class:\n\nuser\n\nUser(name='Alma', email='a@example.com', year_started=2019, pwd=None)\n\n\n\nuser.year_started = 2099\nusers.update(user)\n\nUser(name='Alma', email='a@example.com', year_started=2099, pwd=None)\n\n\nOr use a dict:\n\nusers.update(dict(name='Alma', year_started=2199, email='a@example.com'))\n\nUser(name='Alma', email='a@example.com', year_started=2199, pwd=None)\n\n\nOr use kwargs:\n\nusers.update(name='Alma', year_started=2149)\n\nUser(name='Alma', email='a@example.com', year_started=2149, pwd=None)\n\n\nIf the primary key doesn’t match a record, raise a NotFoundError.\nJohn hasn’t started with us yet so doesn’t get the chance yet to travel in time.\n\ntry: users.update(User(name='John', year_started=2024, email='j@example.com'))\nexcept NotFoundError: print('User not found')\n\nUser not found\n\n\n\n\n.delete()\nDelete a record of the database. Uses the primary key for identifying the record to be removed. Returns a table object.\nCharlie decides to not travel in time. He exits our little group.\n\nusers.delete('Charlie')\n\n<Table user (name, email, year_started, pwd)>\n\n\nIf the primary key value can’t be found, raises a NotFoundError.\n\ntry: users.delete('Charlies')\nexcept NotFoundError: print('User not found')\n\nUser not found\n\n\nIn John’s case, he isn’t time travelling with us yet so can’t be removed.\n\ntry: users.delete('John')\nexcept NotFoundError: print('User not found')\n\nUser not found\n\n\nDeleting records with compound primary keys requires providing the entire key.\n\npublications.delete(['Alma' , 2035])\n\n<Table publication (authors, year, title)>\n\n\n\n\nin keyword\nAre Alma and John contained in the Users table? Or, to be technically precise, is the item with the specified primary key value in this table?\n\n'Alma' in users, 'John' in users\n\n(True, False)\n\n\nAlso works with compound primary keys, as shown below. You’ll note that the operation can be done with either a list or tuple.\n\n['Alma', 2019] in publications\n\nTrue\n\n\nAnd now for a False result, where John has no publications.\n\n('John', 1967) in publications\n\nFalse\n\n\n\n\n.xtra()\nIf we set fields within the .xtra function to a particular value, then indexing is also filtered by those. This applies to every database method except for record creation. This makes it easier to limit users (or other objects) access to only things for which they have permission.\nFor example, if we query all our records below without setting values v ia the .xtra function, we can see todos for everyone. Pay special attention to the id values of all three records, as we are about to filter most of them away.\n\ntodos()\n\n[Todo(id=1, title='Write MiniDataAPI spec', detail=None, status='open', name='Braden'),\n Todo(id=2, title='Implement SSE in FastHTML', detail=None, status='open', name='Alma'),\n Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')]\n\n\nLet’s use .xtra to constrain results just to Charlie. We set the name field in Todos, but it could be any field defined for this table.\n\ntodos.xtra(name='Charlie')\n\nWe’ve now set a field to a value with .xtra, if we loop over all the records again, only those assigned to records with a name of Charlie will be displayed.\n\ntodos()\n\n[Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')]\n\n\nThe in keyword is also affected. Only records with a name of Charlie will evaluate to be True. Let’s demonstrate by testing it with a Charlie record:\n\nct = todos[3]\nct\n\nTodo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')\n\n\nCharlie’s record has an ID of 3. Here we demonstrate that Charlie’s TODO can be found in the list of todos:\n\nct.id in todos\n\nTrue\n\n\nIf we try in with the other IDs the query fails because the filtering is now set to just records with a name of Charlie.\n\n1 in todos, 2 in todos\n\n(False, False)\n\n\n\ntry: todos[2]\nexcept NotFoundError: print('Record not found')\n\nRecord not found\n\n\nWe are also constrained by what records we can update. In the following example we try to update a TODO not named ‘Charlie’. Because the name is wrong, the .update function will raise a NotFoundError.\n\ntry: todos.update(Todo(id=1, title='Finish MiniDataAPI Spec', status='closed', name='Braden'))\nexcept NotFoundError as e: print('Record not updated')\n\nRecord not updated\n\n\nUnlike poor Braden, Charlie isn’t filtered out. Let’s update his TODO.\n\ntodos.update(Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie'))\n\nTodo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')\n\n\nFinally, once constrained by .xtra, only records with Charlie as the name can be deleted.\n\ntry: todos.delete(1)\nexcept NotFoundError as e: print('Record not updated')\n\nRecord not updated\n\n\nCharlie’s TODO was to finish development of FastHTML. While the framework will stabilize, like any good project it will see new features added and the odd bug corrected for many years to come. Therefore, Charlie’s TODO is nonsensical. Let’s delete it.\n\ntodos.delete(ct.id)\n\n<Table todo (id, title, detail, status, name)>\n\n\nWhen a TODO is inserted, the xtra fields are automatically set. This ensures that we don’t accidentally, for instance, insert items for others users. Note that here we don’t set the name field, but it’s still included in the resultant row:\n\nct = todos.insert(Todo(title='Rewrite personal site in FastHTML', status='open'))\nct\n\nTodo(id=3, title='Rewrite personal site in FastHTML', detail=None, status='open', name='Charlie')\n\n\nIf we try to change the username to someone else, the change is ignored, due to xtra:\n\nct.name = 'Braden'\ntodos.update(ct)\n\nTodo(id=3, title='Rewrite personal site in FastHTML', detail=None, status='open', name='Charlie')",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "MiniDataAPI Spec"
+ ]
+ },
+ {
+ "objectID": "explains/minidataapi.html#sql-first-design",
+ "href": "explains/minidataapi.html#sql-first-design",
+ "title": "MiniDataAPI Spec",
+ "section": "SQL-first design",
+ "text": "SQL-first design\n\nusers = None\nUser = None\n\n\nusers = db.t.user\nusers\n\n<Table user (name, email, year_started, pwd)>\n\n\n(This section needs to be documented properly.)\nFrom the table objects we can extract a Dataclass version of our tables. Usually this is given an singular uppercase version of our table name, which in this case is User.\n\nUser = users.dataclass()\n\n\nUser(name='Braden', email='b@example.com', year_started=2018)\n\nUser(name='Braden', email='b@example.com', year_started=2018, pwd=UNSET)",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "MiniDataAPI Spec"
+ ]
+ },
+ {
+ "objectID": "explains/minidataapi.html#implementations",
+ "href": "explains/minidataapi.html#implementations",
+ "title": "MiniDataAPI Spec",
+ "section": "Implementations",
+ "text": "Implementations\n\nImplementing MiniDataAPI for a new datastore\nFor creating new implementations, the code examples in this specification are the test case for the API. New implementations should pass the tests in order to be compliant with the specification.\n\n\nImplementations\n\nfastlite - The original implementation, only for Sqlite\nfastsql - An SQL database agnostic implementation based on the excellent SQLAlchemy library.",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "MiniDataAPI Spec"
+ ]
+ },
+ {
+ "objectID": "explains/websockets.html",
+ "href": "explains/websockets.html",
+ "title": "WebSockets",
+ "section": "",
+ "text": "Websockets are a protocol for two-way, persistent communication between a client and server. This is different from HTTP, which uses a request/response model where the client sends a request and the server responds. With websockets, either party can send messages at any time, and the other party can respond.\nThis allows for different applications to be built, including things like chat apps, live-updating dashboards, and real-time collaborative tools, which would require constant polling of the server for updates with HTTP.\nIn FastHTML, you can create a websocket route using the @app.ws decorator. This decorator takes a route path, and optional conn and disconn parameters representing the on_connect and on_disconnect callbacks in websockets, respectively. The function decorated by @app.ws is the main function that is called when a message is received.\nHere’s an example of a basic websocket route:\nThe on_message function is the main function that is called when a message is received and can be named however you like. Similar to standard routes, the arguments to on_message are automatically parsed from the websocket payload for you, so you don’t need to manually parse the message content. However, certain argument names are reserved for special purposes. Here are the most important ones:\nFor example, we can send a message to the client that just connected like this:\nOr if we receive a message from the client, we can send a message back to them:\nOn the client side, we can use HTMX’s websocket extension to open a websocket connection and send/receive messages. For example:\nThis will create a websocket connection to the server on route /ws, and send any form submissions to the server via the websocket. The server will then respond by sending a message back to the client. The client will then update the message div with the message from the server using Out of Band Swaps, which means that the content is swapped with the same id without reloading the page.\nPutting it all together, the code for the client and server should look like this:\nThis is a fairly simple example and could be done just as easily with standard HTTP requests, but it illustrates the basic idea of how websockets work. Let’s look at a more complex example next.",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "WebSockets"
+ ]
+ },
+ {
+ "objectID": "explains/websockets.html#real-time-chat-app",
+ "href": "explains/websockets.html#real-time-chat-app",
+ "title": "WebSockets",
+ "section": "Real-Time Chat App",
+ "text": "Real-Time Chat App\nLet’s put our new websocket knowledge to use by building a simple chat app. We will create a chat app where multiple users can send and receive messages in real time.\nLet’s start by defining the app and the home page:\nfrom fasthtml.common import *\n\napp = FastHTML(ws_hdr=True)\nrt = app.route\n\nmsgs = []\n@rt('/')\ndef home(): return Div(\n Div(Ul(*[Li(m) for m in msgs], id='msg-list')),\n Form(Input(id='msg'), id='form', ws_send=True),\n hx_ext='ws', ws_connect='/ws')\nNow, let’s handle the websocket connection. We’ll add a new route for this along with an on_conn and on_disconn function to keep track of the users currently connected to the websocket. Finally, we will handle the logic for sending messages to all connected users.\nusers = {}\ndef on_conn(ws, send): users[str(id(ws))] = send\ndef on_disconn(ws): users.pop(str(id(ws)), None)\n\n@app.ws('/ws', conn=on_conn, disconn=on_disconn)\nasync def ws(msg:str):\n msgs.append(msg)\n # Use associated `send` function to send message to each user\n for u in users.values(): await u(Ul(*[Li(m) for m in msgs], id='msg-list'))\n\nserve()\nWe can now run this app with python chat_ws.py and open multiple browser tabs to http://localhost:5001. You should be able to send messages in one tab and see them appear in the other tabs.\n\nA Work in Progress\nThis page (and Websocket support in FastHTML) is a work in progress. Questions, PRs, and feedback are welcome!",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "WebSockets"
+ ]
+ },
+ {
+ "objectID": "explains/oauth.html",
+ "href": "explains/oauth.html",
+ "title": "OAuth",
+ "section": "",
+ "text": "OAuth is an open standard for ‘access delegation’, commonly used as a way for Internet users to grant websites or applications access to their information on other websites but without giving them the passwords. It is the mechanism that enables “Log in with Google” on many sites, saving you from having to remember and manage yet another password. Like many auth-related topics, there’s a lot of depth and complexity to the OAuth standard, but once you understand the basic usage it can be a very convenient alternative to managing your own user accounts.\nOn this page you’ll see how to use OAuth with FastHTML to implement some common pieces of functionality.\nIn FastHTML you set up a client like GoogleAppClient. The client is responsible for storing the client ID and client secret, and for handling the OAuth flow. Let’s run through three examples, illustrating some important concepts across three different OAuth providers.",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "OAuth"
+ ]
+ },
+ {
+ "objectID": "explains/oauth.html#a-minimal-login-flow-github",
+ "href": "explains/oauth.html#a-minimal-login-flow-github",
+ "title": "OAuth",
+ "section": "A Minimal Login Flow (GitHub)",
+ "text": "A Minimal Login Flow (GitHub)\nLet’s begin by building a minimal ‘Sign in with GitHub’ flow. This will demonstrate the basic steps of OAuth.\nOAuth requires a “provider” (in this case, GitHub) to authenticate the user. So the first step when setting up our app is to register with GitHub to set things up.\nGo to https://github.com/settings/developers and click “New OAuth App”. Fill in the form with the following values, then click ‘Register application’.\n\nApplication name: Your app name\nHomepage URL: http://localhost:8000 (or whatever URL you’re using - you can change this later)\nAuthorization callback URL: http://localhost:8000/auth_redirect (you can modify this later too)\n\n\n\n\nAfter you register, you’ll see a screen where you can view the client ID and generate a client secret. Store these values in a safe place. You’ll use them to create a GitHubAppClient object in FastHTML.\nThis client object is responsible for handling the parts of the OAuth flow which depend on direct communication between your app and GitHub, as opposed to interactions which go through the user’s browser via redirects.\nHere is how to setup the client object:\nclient = GitHubAppClient(\n client_id=\"your_client_id\",\n client_secret=\"your_client_secret\"\n)\nYou should also save the path component of the authorization callback URL which you provided on registration.\nThis route is where GitHub will redirect the user’s browser in order to send an authorization code to your app. You should save only the URL’s path component rather than the entire URL because you want your code to work automatically in deployment, when the host and port part of the URL change from localhost:8000 to your real DNS name.\nSave the special authorization callback path under an obvious name:\nauth_callback_path = \"/auth_redirect\"\n\n\n\n\n\n\nNote\n\n\n\nIt’s recommended to store the client ID, and secret, in environment variables, rather than hardcoding them in your code.\n\n\nWhen the user visit a normal page of your app, if they are not already logged in, then you’ll want to redirect them to your app’s login page, which will live at the /login path. We accomplish that by using this piece of “beforeware”, which defines logic which runs before other work for all routes except ones we specify to be skipped:\ndef before(req, session):\n auth = req.scope['auth'] = session.get('user_id', None)\n if not auth: return RedirectResponse('/login', status_code=303)\n counts.xtra(name=auth)\nbware = Beforeware(before, skip=['/login', auth_callback_path])\nWe configure the beforeware to skip /login because that’s where the user goes to login, and we also skip the special authorization callback path because that is used by OAuth itself to receive information from GitHub.\nIt’s only at your login page that we start the OAuth flow. To start the OAuth flow, you need to give the user a link to GitHub’s login for your app. You’ll need the client object to generate that link, and the client object will in turn need the full authorization callback URL, which we need to build from the authorization callback path, so it is a multi-step process to produce this GitHub login link.\nHere is an implementation of your own /login route handler. It generates the GitHub login link and presents it to the user:\n@app.get('/login')\ndef login(request)\n redir = redir_url(request,auth_callback_path)\n login_link = client.login_link(redir)\n return P(A('Login with GitHub', href=login_link)) \nOnce the user follows that link, GitHub will ask them to grant permission to your app to access their GitHub account. If they agree, GitHub will redirect them back to your app’s authorization callback URL, carrying an authorization code which your app can use to generate an access token. To receive this code, you need to set up a route in FastHTML that listens for requests at the authorization callback path. For example:\n@app.get(auth_callback_path)\ndef auth_redirect(code:str):\n return P(f\"code: {code}\")\nThis authorization code is temporary, and is used by your app to directly ask the provider for user information like an access token.\nTo recap, you can think of the exchange so far as:\n\nUser to us: “I want to log in with you, app.”\nUs to User: “Okay but first, here’s a special link to log in with GitHub”\nUser to GitHub: “I want to log in with you, GitHub, to use this app.”\nGitHub to User: “OK, redirecting you back to the app’s URL (with an auth code)”\nUser to Us: “Hi again, app. Here’s the GitHub auth code you need to ask GitHub for info about me” (delivered via /auth_redirect?code=...)\n\nThe final steps we need to implement are as follows:\n\nUs to GitHUb: “A user just gave me this auth code. May I have the user info (e.g., an access token)?”\nGitHub to us: “Since you have an auth code, here’s the user info”\n\nIt’s critical for us to derive the user info from the auth code immediately in the authorization callback, because the auth code may be used only once. So we use it that once in order to get information like an access token, which will remain valid for longer.\nTo go from the auth code to user info, you use info = client.retr_info(code,redirect_uri). From the user info, you can extract the user_id, which is a unique identifier for the user:\n@app.get(auth_callback_path)\ndef auth_redirect(code:str, request):\n redir = redir_url(request, auth_callback_path)\n user_info = client.retr_info(code, redir)\n user_id = info[client.id_key]\n return P(f\"User id: {user_id}\")\nBut we want the user ID not to print it but to remember the user.\nSo let us store it in the session object, to remember who is logged in:\n@app.get(auth_callback_path)\ndef auth_redirect(code:str, request, session):\n redir = redir_url(request, auth_callback_path)\n user_info = client.retr_info(code, redir)\n user_id = user_info[client.id_key] # get their ID\n session['user_id'] = user_id # save ID in the session\n return RedirectResponse('/', status_code=303)\nThe session object is derived from values visible to the user’s browser, but it is cryptographically signed so the user can’t read it themselves. This makes it safe to store even information we don’t want to expose to the user.\nFor larger quantities of data, we’d want to save that information in a database and use the session to hold keys to lookup information from that database.\nHere’s a minimal app that puts all these pieces together. It uses the user info to get the user_id. It stores that in the session object. It then uses the user_id as a key into a database, which tracks how frequently every user has hit an increment button.\nimport os\nfrom fasthtml.common import *\nfrom fasthtml.oauth import GitHubAppClient, redir_url\n\ndb = database('data/counts.db')\ncounts = db.t.counts\nif counts not in db.t: counts.create(dict(name=str, count=int), pk='name')\nCount = counts.dataclass()\n\n# Auth client setup for GitHub\nclient = GitHubAppClient(os.getenv(\"AUTH_CLIENT_ID\"), \n os.getenv(\"AUTH_CLIENT_SECRET\"))\nauth_callback_path = \"/auth_redirect\"\n\ndef before(req, session):\n # if not logged in, we send them to our login page\n # logged in means:\n # - 'user_id' in the session object, \n # - 'auth' in the request object\n auth = req.scope['auth'] = session.get('user_id', None)\n if not auth: return RedirectResponse('/login', status_code=303)\n counts.xtra(name=auth)\nbware = Beforeware(before, skip=['/login', auth_callback_path])\n\napp = FastHTML(before=bware)\n\n# User asks us to Login\n@app.get('/login')\ndef login(request):\n redir = redir_url(request,auth_callback_path)\n login_link = client.login_link(redir)\n # we tell user to login at github\n return P(A('Login with GitHub', href=login_link)) \n\n# User comes back to us with an auth code from Github\n@app.get(auth_callback_path)\ndef auth_redirect(code:str, request, session):\n redir = redir_url(request, auth_callback_path)\n user_info = client.retr_info(code, redir)\n user_id = user_info[client.id_key] # get their ID\n session['user_id'] = user_id # save ID in the session\n # create a db entry for the user\n if user_id not in counts: counts.insert(name=user_id, count=0)\n return RedirectResponse('/', status_code=303)\n\n@app.get('/')\ndef home(auth):\n return Div(\n P(\"Count demo\"),\n P(f\"Count: \", Span(counts[auth].count, id='count')),\n Button('Increment', hx_get='/increment', hx_target='#count'),\n P(A('Logout', href='/logout'))\n )\n\n@app.get('/increment')\ndef increment(auth):\n c = counts[auth]\n c.count += 1\n return counts.upsert(c).count\n\n@app.get('/logout')\ndef logout(session):\n session.pop('user_id', None)\n return RedirectResponse('/login', status_code=303)\n\nserve()\nSome things to note:\n\nThe before function is used to check if the user is authenticated. If not, they are redirected to the login page.\nTo log the user out, we remove the user ID from the session.\nCalling counts.xtra(name=auth) ensures that only the row corresponding to the current user is accessible when responding to a request. This is often nicer than trying to remember to filter the data in every route, and lowers the risk of accidentally leaking data.\nIn the auth_redirect route, we store the user ID in the session and create a new row in the user_counts table if it doesn’t already exist.\n\nYou can find more heavily-commented version of this code in the oauth directory in fasthtml-example, along with an even more minimal example. More examples may be added in the future.\n\nRevoking Tokens (Google)\nWhen the user in the example above logs out, we remove their user ID from the session. However, the user is still logged in to GitHub. If they click ‘Login with GitHub’ again, they’ll be redirected back to our site without having to log in again. This is because GitHub remembers that they’ve already granted our app permission to access their account. Most of the time this is convenient, but for testing or security purposes you may want a way to revoke this permission.\nAs a user, you can usually revoke access to an app from the provider’s website (for example, https://github.com/settings/applications). But as a developer, you can also revoke access programmatically - at least with some providers. This requires keeping track of the access token (stored in client.token[\"access_token\"] after you call retr_info), and sending a request to the provider’s revoke URL:\nauth_revoke_url = \"https://accounts.google.com/o/oauth2/revoke\"\ndef revoke_token(token):\n response = requests.post(auth_revoke_url, params={\"token\": token})\n return response.status_code == 200 # True if successful\nNot all providers support token revocation, and it is not built into FastHTML clients at the moment.\n\n\nUsing State (Hugging Face)\nImagine a user (not logged in) comes to your AI image editing site, starts testing things out, and then realizes they need to sign in before they can click “Run (Pro)” on the edit they’re working on. They click “Sign in with Hugging Face”, log in, and are redirected back to your site. But now they’ve lost their in-progress edit and are left just looking at the homepage! This is an example of a case where you might want to keep track of some additional state. Another strong use case for being able to pass some uniqie state through the OAuth flow is to prevent something called a CSRF attack. To add a state string to the OAuth flow, you can use client.login_link_with_state(state) instead of client.login_link(), like so:\n# in login page:\nlink = A('Login with GitHub', href=client.login_link_with_state(state='current_prompt: add a unicorn'))\n\n# in auth_redirect:\n@app.get('/auth_redirect')\ndef auth_redirect(code:str, session, state:str=None):\n print(f\"state: {state}\") # Use as needed\n ...\nThe state string is passed through the OAuth flow and back to your site.\n\n\nA Work in Progress\nThis page (and OAuth support in FastHTML) is a work in progress. Questions, PRs, and feedback are welcome!",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "OAuth"
+ ]
+ },
+ {
+ "objectID": "api/components.html",
+ "href": "api/components.html",
+ "title": "Components",
+ "section": "",
+ "text": "from lxml import html as lx\nfrom pprint import pprint\n\n\nsource\n\nshow\n\n show (ft, *rest)\n\nRenders FT Components into HTML within a Jupyter notebook.\n\nsentence = P(Strong(\"FastHTML is \", I(\"Fast\")))\n\n# When placed within the `show()` function, this will render\n# the HTML in Jupyter notebooks.\nshow(sentence)\n\n\nFastHTML is Fast\n\n\n\n# Called without the `show()` function, the raw HTML is displayed\nsentence\n\n<p>\n<strong>FastHTML is <i>Fast</i></strong></p>\n\n\n\nsource\n\n\nattrmap_x\n\n attrmap_x (o)\n\n\nsource\n\n\nft_html\n\n ft_html (tag:str, *c, id=None, cls=None, title=None, style=None,\n attrmap=None, valmap=None, ft_cls=None, auto_id=None, **kwargs)\n\n\nsource\n\n\nft_hx\n\n ft_hx (tag:str, *c, target_id=None, hx_vals=None, id=None, cls=None,\n title=None, style=None, accesskey=None, contenteditable=None,\n dir=None, draggable=None, enterkeyhint=None, hidden=None,\n inert=None, inputmode=None, lang=None, popover=None,\n spellcheck=None, tabindex=None, translate=None, hx_get=None,\n hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,\n hx_trigger=None, hx_target=None, hx_swap=None, hx_swap_oob=None,\n hx_include=None, hx_select=None, hx_select_oob=None,\n hx_indicator=None, hx_push_url=None, hx_confirm=None,\n hx_disable=None, hx_replace_url=None, hx_disabled_elt=None,\n hx_ext=None, hx_headers=None, hx_history=None,\n hx_history_elt=None, hx_inherit=None, hx_params=None,\n hx_preserve=None, hx_prompt=None, hx_request=None, hx_sync=None,\n hx_validate=None, **kwargs)\n\n\nft_html('a', _at_click_dot_away=1)\n\n<a @click_dot_away=\"1\"></a>\n\n\n\nft_html('a', **{'@click.away':1})\n\n<a @click.away=\"1\"></a>\n\n\n\nft_html('a', {'@click.away':1})\n\n<a @click.away=\"1\"></a>\n\n\n\nft_hx('a', hx_vals={'a':1})\n\n<a hx-vals='{\"a\": 1}'></a>\n\n\n\nsource\n\n\nFile\n\n File (fname)\n\nUse the unescaped text in file fname directly\nFor tags that have a name attribute, it will be set to the value of id if not provided explicitly:\n\nForm(Button(target_id='foo', id='btn'),\n hx_post='/', target_id='tgt', id='frm')\n\n<form hx-post=\"/\" hx-target=\"#tgt\" id=\"frm\" name=\"frm\"><button hx-target=\"#foo\" id=\"btn\" name=\"btn\"></button></form>\n\n\n\nsource\n\n\nfill_form\n\n fill_form (form:fastcore.xml.FT, obj)\n\nFills named items in form using attributes in obj\n\n@dataclass\nclass TodoItem:\n title:str; id:int; done:bool; details:str; opt:str='a'\n\ntodo = TodoItem(id=2, title=\"Profit\", done=True, details=\"Details\", opt='b')\ncheck = Label(Input(type=\"checkbox\", cls=\"checkboxer\", name=\"done\", data_foo=\"bar\"), \"Done\", cls='px-2')\nform = Form(Fieldset(Input(cls=\"char\", id=\"title\", value=\"a\"), check, Input(type=\"hidden\", id=\"id\"),\n Select(Option(value='a'), Option(value='b'), name='opt'),\n Textarea(id='details'), Button(\"Save\"),\n name=\"stuff\"))\nform = fill_form(form, todo)\nassert '<textarea id=\"details\" name=\"details\">Details</textarea>' in to_xml(form)\nform\n\n<form><fieldset name=\"stuff\"> <input value=\"Profit\" id=\"title\" class=\"char\" name=\"title\">\n<label class=\"px-2\"> <input type=\"checkbox\" name=\"done\" data-foo=\"bar\" class=\"checkboxer\" checked=\"1\">\nDone</label> <input type=\"hidden\" id=\"id\" name=\"id\" value=\"2\">\n<select name=\"opt\"><option value=\"a\"></option><option value=\"b\" selected=\"1\"></option></select><textarea id=\"details\" name=\"details\">Details</textarea><button>Save</button></fieldset></form>\n\n\n\nsource\n\n\nfill_dataclass\n\n fill_dataclass (src, dest)\n\nModifies dataclass in-place and returns it\n\nnt = TodoItem('', 0, False, '')\nfill_dataclass(todo, nt)\nnt\n\nTodoItem(title='Profit', id=2, done=True, details='Details', opt='b')\n\n\n\nsource\n\n\nfind_inputs\n\n find_inputs (e, tags='input', **kw)\n\nRecursively find all elements in e with tags and attrs matching kw\n\ninps = find_inputs(form, id='title')\ntest_eq(len(inps), 1)\ninps\n\n[input((),{'value': 'Profit', 'id': 'title', 'class': 'char', 'name': 'title'})]\n\n\nYou can also use lxml for more sophisticated searching:\n\nelem = lx.fromstring(to_xml(form))\ntest_eq(elem.xpath(\"//input[@id='title']/@value\"), ['Profit'])\n\n\nsource\n\n\ngetattr\n\n __getattr__ (tag)\n\n\nsource\n\n\nhtml2ft\n\n html2ft (html, attr1st=False)\n\nConvert HTML to an ft expression\n\nh = to_xml(form)\nhl_md(html2ft(h), 'python')\n\nForm(\n Fieldset(\n Input(value='Profit', id='title', name='title', cls='char'),\n Label(\n Input(type='checkbox', name='done', data_foo='bar', checked='1', cls='checkboxer'),\n 'Done',\n cls='px-2'\n ),\n Input(type='hidden', id='id', name='id', value='2'),\n Select(\n Option(value='a'),\n Option(value='b', selected='1'),\n name='opt'\n ),\n Textarea('Details', id='details', name='details'),\n Button('Save'),\n name='stuff'\n )\n)\n\n\n\nhl_md(html2ft(h, attr1st=True), 'python')\n\nForm(\n Fieldset(name='stuff')(\n Input(value='Profit', id='title', name='title', cls='char'),\n Label(cls='px-2')(\n Input(type='checkbox', name='done', data_foo='bar', checked='1', cls='checkboxer'),\n 'Done'\n ),\n Input(type='hidden', id='id', name='id', value='2'),\n Select(name='opt')(\n Option(value='a'),\n Option(value='b', selected='1')\n ),\n Textarea('Details', id='details', name='details'),\n Button('Save')\n )\n)\n\n\n\nsource\n\n\nsse_message\n\n sse_message (elm, event='message')\n\nConvert element elm into a format suitable for SSE streaming\n\nprint(sse_message(Div(P('hi'), P('there'))))\n\nevent: message\ndata: <div>\ndata: <p>hi</p>\ndata: <p>there</p>\ndata: </div>",
+ "crumbs": [
+ "Home",
+ "Source",
+ "Components"
+ ]
+ },
+ {
+ "objectID": "api/jupyter.html",
+ "href": "api/jupyter.html",
+ "title": "Jupyter compatibility",
+ "section": "",
+ "text": "from httpx import get, AsyncClient",
+ "crumbs": [
+ "Home",
+ "Source",
+ "Jupyter compatibility"
+ ]
+ },
+ {
+ "objectID": "api/jupyter.html#helper-functions",
+ "href": "api/jupyter.html#helper-functions",
+ "title": "Jupyter compatibility",
+ "section": "Helper functions",
+ "text": "Helper functions\n\nsource\n\nnb_serve\n\n nb_serve (app, log_level='error', port=8000, **kwargs)\n\nStart a Jupyter compatible uvicorn server with ASGI app on port with log_level\n\nsource\n\n\nnb_serve_async\n\n nb_serve_async (app, log_level='error', port=8000, **kwargs)\n\nAsync version of nb_serve\n\nsource\n\n\nis_port_free\n\n is_port_free (port, host='localhost')\n\nCheck if port is free on host\n\nsource\n\n\nwait_port_free\n\n wait_port_free (port, host='localhost', max_wait=3)\n\nWait for port to be free on host",
+ "crumbs": [
+ "Home",
+ "Source",
+ "Jupyter compatibility"
+ ]
+ },
+ {
+ "objectID": "api/jupyter.html#using-fasthtml-in-jupyter",
+ "href": "api/jupyter.html#using-fasthtml-in-jupyter",
+ "title": "Jupyter compatibility",
+ "section": "Using FastHTML in Jupyter",
+ "text": "Using FastHTML in Jupyter\n\nsource\n\nJupyUvi\n\n JupyUvi (app, log_level='error', port=8000, start=True, **kwargs)\n\nStart and stop a Jupyter compatible uvicorn server with ASGI app on port with log_level\nCreating an object of this class also starts the Uvicorn server. It runs in a separate thread, so you can use normal HTTP client functions in a notebook.\n\napp = FastHTML()\n\n@app.route\ndef index(): return 'hi'\n\nport = 8000\nserver = JupyUvi(app, port=port)\n\n\nget(f'http://localhost:{port}').text\n\n'hi'\n\n\nYou can stop the server, modify routes, and start the server again without restarting the notebook or recreating the server or application.\n\nserver.stop()\n\n\nsource\n\n\nFastJupy\n\n FastJupy (hdrs=None, middleware=None, **kwargs)\n\nSame as FastHTML, but with Jupyter compatible middleware and headers added\nInstead of using the FastHTML class, use the FastJupy class. It’s a thin wrapper for FastHTML which adds the necessary headers and middleware required for Jupyter compatibility.\n\napp = FastJupy()\nrt = app.route\nserver = JupyUvi(app, port=port)\n\n\nsource\n\n\nHTMX\n\n HTMX (path='', host='localhost', port=8000, iframe_height='auto')\n\nAn iframe which displays the HTMX application in a notebook.\n\n@rt\ndef index():\n return Div(\n P(A('Click me', hx_get=update, hx_target='#result')),\n P(A('No me!', hx_get=update, hx_target='#result')),\n Div(id='result'))\n\n@rt\ndef update(): return Div(P('Hi!'),P('There!'))\n\n\n# Run the notebook locally to see the HTMX iframe in action\n# HTMX()\n\n# If only part of the page gets displayed, try tweaking the iframe_height parameter, for example:\n# HTMX(iframe_height=\"800px\")\n\n\nserver.stop()",
+ "crumbs": [
+ "Home",
+ "Source",
+ "Jupyter compatibility"
+ ]
+ },
+ {
+ "objectID": "api/jupyter.html#jupy_app",
+ "href": "api/jupyter.html#jupy_app",
+ "title": "Jupyter compatibility",
+ "section": "jupy_app",
+ "text": "jupy_app\n\nsource\n\njupy_app\n\n jupy_app (pico=False, hdrs=None, middleware=None, **kwargs)\n\nSame as fast_app but for Jupyter notebooks\n\napp,rt = jupy_app()\nserver = JupyUvi(app)\n\n\n@rt\ndef index():\n return Div(\n P(A('Click me', hx_get=update, hx_target='#result')),\n P(A('No me!', hx_get=update, hx_target='#result')),\n Div(id='result'))\n\n@rt\ndef update(): return Div(P('Hi!'),P('Rachel!'))\n\n\n# Run the notebook locally to see the HTMX iframe in action\n# HTMX()\n# HTMX(update)\n# HTMX('/update')\n\n\nserver.stop()",
+ "crumbs": [
+ "Home",
+ "Source",
+ "Jupyter compatibility"
+ ]
+ },
+ {
+ "objectID": "api/fastapp.html",
+ "href": "api/fastapp.html",
+ "title": "fastapp",
+ "section": "",
+ "text": "Usage can be summarized as:\nfrom fasthtml.common import *\n\napp, rt = fast_app()\n\n@rt\ndef index(): return Titled(\"A demo of fast_app()@\")\n\nserve()\n\nsource\n\nfast_app\n\n fast_app (db_file:Optional[str]=None, render:Optional[<built-\n infunctioncallable>]=None, hdrs:Optional[tuple]=None,\n ftrs:Optional[tuple]=None, tbls:Optional[dict]=None,\n before:Union[tuple,NoneType,fasthtml.core.Beforeware]=None,\n middleware:Optional[tuple]=None, live:bool=False,\n debug:bool=False, routes:Optional[tuple]=None,\n exception_handlers:Optional[dict]=None,\n on_startup:Optional[<built-infunctioncallable>]=None,\n on_shutdown:Optional[<built-infunctioncallable>]=None,\n lifespan:Optional[<built-infunctioncallable>]=None,\n default_hdrs=True, pico:Optional[bool]=None,\n surreal:Optional[bool]=True, htmx:Optional[bool]=True,\n ws_hdr:bool=False, secret_key:Optional[str]=None,\n key_fname:str='.sesskey', session_cookie:str='session_',\n max_age:int=31536000, sess_path:str='/', same_site:str='lax',\n sess_https_only:bool=False, sess_domain:Optional[str]=None,\n htmlkw:Optional[dict]=None, bodykw:Optional[dict]=None,\n reload_attempts:Optional[int]=1,\n reload_interval:Optional[int]=1000, static_path:str='.',\n **kwargs)\n\nCreate a FastHTML or FastHTMLWithLiveReload app.\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\ndb_file\nOptional\nNone\nDatabase file name, if needed\n\n\nrender\nOptional\nNone\nFunction used to render default database class\n\n\nhdrs\nOptional\nNone\nAdditional FT elements to add to \n\n\nftrs\nOptional\nNone\nAdditional FT elements to add to end of \n\n\ntbls\nOptional\nNone\nExperimental mapping from DB table names to dict table definitions\n\n\nbefore\nUnion\nNone\nFunctions to call prior to calling handler\n\n\nmiddleware\nOptional\nNone\nStandard Starlette middleware\n\n\nlive\nbool\nFalse\nEnable live reloading\n\n\ndebug\nbool\nFalse\nPassed to Starlette, indicating if debug tracebacks should be returned on errors\n\n\nroutes\nOptional\nNone\nPassed to Starlette\n\n\nexception_handlers\nOptional\nNone\nPassed to Starlette\n\n\non_startup\nOptional\nNone\nPassed to Starlette\n\n\non_shutdown\nOptional\nNone\nPassed to Starlette\n\n\nlifespan\nOptional\nNone\nPassed to Starlette\n\n\ndefault_hdrs\nbool\nTrue\nInclude default FastHTML headers such as HTMX script?\n\n\npico\nOptional\nNone\nInclude PicoCSS header?\n\n\nsurreal\nOptional\nTrue\nInclude surreal.js/scope headers?\n\n\nhtmx\nOptional\nTrue\nInclude HTMX header?\n\n\nws_hdr\nbool\nFalse\nInclude HTMX websocket extension header?\n\n\nsecret_key\nOptional\nNone\nSigning key for sessions\n\n\nkey_fname\nstr\n.sesskey\nSession cookie signing key file name\n\n\nsession_cookie\nstr\nsession_\nSession cookie name\n\n\nmax_age\nint\n31536000\nSession cookie expiry time\n\n\nsess_path\nstr\n/\nSession cookie path\n\n\nsame_site\nstr\nlax\nSession cookie same site policy\n\n\nsess_https_only\nbool\nFalse\nSession cookie HTTPS only?\n\n\nsess_domain\nOptional\nNone\nSession cookie domain\n\n\nhtmlkw\nOptional\nNone\nAttrs to add to the HTML tag\n\n\nbodykw\nOptional\nNone\nAttrs to add to the Body tag\n\n\nreload_attempts\nOptional\n1\nNumber of reload attempts when live reloading\n\n\nreload_interval\nOptional\n1000\nTime between reload attempts in ms\n\n\nstatic_path\nstr\n.\nWhere the static file route points to, defaults to root dir\n\n\nkwargs\n\n\n\n\n\nReturns\nAny",
+ "crumbs": [
+ "Home",
+ "Source",
+ "fastapp"
+ ]
+ },
+ {
+ "objectID": "api/js.html",
+ "href": "api/js.html",
+ "title": "Javascript examples",
+ "section": "",
+ "text": "To expedite fast development, FastHTML comes with several built-in Javascript and formatting components. These are largely provided to demonstrate FastHTML JS patterns. There’s far too many JS libs for FastHTML to wrap them all, and as shown here the code to add FastHTML support is very simple anyway.\n\nsource\n\nlight_media\n\n light_media (css:str)\n\nRender light media for day mode views\n\n\n\n\nType\nDetails\n\n\n\n\ncss\nstr\nCSS to be included in the light media query\n\n\n\n\nlight_media('.body {color: green;}')\n\n<style>@media (prefers-color-scheme: light) {.body {color: green;}}</style>\n\n\n\nsource\n\n\ndark_media\n\n dark_media (css:str)\n\nRender dark media for nught mode views\n\n\n\n\nType\nDetails\n\n\n\n\ncss\nstr\nCSS to be included in the dark media query\n\n\n\n\ndark_media('.body {color: white;}')\n\n<style>@media (prefers-color-scheme: dark) {.body {color: white;}}</style>\n\n\n\nsource\n\n\nMarkdownJS\n\n MarkdownJS (sel='.marked')\n\nImplements browser-based markdown rendering.\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nsel\nstr\n.marked\nCSS selector for markdown elements\n\n\n\nUsage example here.\n\n__file__ = '../../fasthtml/katex.js'\n\n\nsource\n\n\nKatexMarkdownJS\n\n KatexMarkdownJS (sel='.marked', inline_delim='$', display_delim='$$',\n math_envs=None)\n\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nsel\nstr\n.marked\nCSS selector for markdown elements\n\n\ninline_delim\nstr\n$\nDelimiter for inline math\n\n\ndisplay_delim\nstr\n$$\nDelimiter for long math\n\n\nmath_envs\nNoneType\nNone\nList of environments to render as display math\n\n\n\nKatexMarkdown usage example:\nlongexample = r\"\"\"\nLong example:\n\n$$\\begin{array}{c}\n\n\\nabla \\times \\vec{\\mathbf{B}} -\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{E}}}{\\partial t} &\n= \\frac{4\\pi}{c}\\vec{\\mathbf{j}} \\nabla \\cdot \\vec{\\mathbf{E}} & = 4 \\pi \\rho \\\\\n\n\\nabla \\times \\vec{\\mathbf{E}}\\, +\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{B}}}{\\partial t} & = \\vec{\\mathbf{0}} \\\\\n\n\\nabla \\cdot \\vec{\\mathbf{B}} & = 0\n\n\\end{array}$$\n\"\"\"\n\napp, rt = fast_app(hdrs=[KatexMarkdownJS()])\n\n@rt('/')\ndef get():\n return Titled(\"Katex Examples\", \n # Assigning 'marked' class to components renders content as markdown\n P(cls='marked')(\"Inline example: $\\sqrt{3x-1}+(1+x)^2$\"),\n Div(cls='marked')(longexample)\n )\n\nsource\n\n\nHighlightJS\n\n HighlightJS (sel='pre code', langs:str|list|tuple='python', light='atom-\n one-light', dark='atom-one-dark')\n\nImplements browser-based syntax highlighting. Usage example here.\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nsel\nstr\npre code\nCSS selector for code elements. Default is industry standard, be careful before adjusting it\n\n\nlangs\nstr | list | tuple\npython\nLanguage(s) to highlight\n\n\nlight\nstr\natom-one-light\nLight theme\n\n\ndark\nstr\natom-one-dark\nDark theme\n\n\n\n\nsource\n\n\nSortableJS\n\n SortableJS (sel='.sortable', ghost_class='blue-background-class')\n\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nsel\nstr\n.sortable\nCSS selector for sortable elements\n\n\nghost_class\nstr\nblue-background-class\nWhen an element is being dragged, this is the class used to distinguish it from the rest",
+ "crumbs": [
+ "Home",
+ "Source",
+ "Javascript examples"
+ ]
+ },
+ {
+ "objectID": "api/oauth.html",
+ "href": "api/oauth.html",
+ "title": "OAuth",
+ "section": "",
+ "text": "This is not yet thoroughly tested. See the docs page for an explanation of how to use this.\n\nfrom IPython.display import Markdown\n\n\nsource\n\nGoogleAppClient\n\n GoogleAppClient (client_id, client_secret, code=None, scope=None,\n **kwargs)\n\nA WebApplicationClient for Google oauth2\n\nsource\n\n\nGitHubAppClient\n\n GitHubAppClient (client_id, client_secret, code=None, scope=None,\n **kwargs)\n\nA WebApplicationClient for GitHub oauth2\n\nsource\n\n\nHuggingFaceClient\n\n HuggingFaceClient (client_id, client_secret, code=None, scope=None,\n state=None, **kwargs)\n\nA WebApplicationClient for HuggingFace oauth2\n\nsource\n\n\nDiscordAppClient\n\n DiscordAppClient (client_id, client_secret, is_user=False, perms=0,\n scope=None, **kwargs)\n\nA WebApplicationClient for Discord oauth2\n\ncli = GoogleAppClient.from_file('/Users/jhoward/git/_nbs/oauth-test/client_secret.json')\n\n\nsource\n\n\nWebApplicationClient.login_link\n\n WebApplicationClient.login_link (redirect_uri, scope=None, state=None)\n\nGet a login link for this client\nGenerating a login link that sends the user to the OAuth provider is done with client.login_link().\nIt can sometimes be useful to pass state to the OAuth provider, so that when the user returns you can pick up where they left off. This can be done by passing the state parameter.\n\nfrom fasthtml.common import *\nfrom fasthtml.jupyter import *\n\n\nredir_path = '/redirect'\nport = 8000\ncode_stor = None\n\n\napp,rt = jupy_app()\nserver = JupyUvi(app, port=port)\n\n\nsource\n\n\nredir_url\n\n redir_url (request, redir_path, scheme='https')\n\nGet the redir url for the host in request\n\n@rt\ndef index(request):\n redir = redir_url(request, redir_path)\n return A('login', href=cli.login_link(redir), target='_blank')\n\n\nsource\n\n\n_AppClient.parse_response\n\n _AppClient.parse_response (code, redirect_uri)\n\nGet the token from the oauth2 server response\n\nsource\n\n\n_AppClient.get_info\n\n _AppClient.get_info (token=None)\n\nGet the info for authenticated user\n\nsource\n\n\n_AppClient.retr_info\n\n _AppClient.retr_info (code, redirect_uri)\n\nCombines parse_response and get_info\n\n@rt(redir_path)\ndef get(request, code:str):\n redir = redir_url(request, redir_path)\n info = cli.retr_info(code, redir)\n return P(f'Login successful for {info[\"name\"]}!')\n\n\nHTMX()\n\n \n\n\n\nserver.stop()\n\n\nsource\n\n\n_AppClient.retr_id\n\n _AppClient.retr_id (code, redirect_uri)\n\nCall retr_info and then return id/subscriber value\nAfter logging in via the provider, the user will be redirected back to the supplied redirect URL. The request to this URL will contain a code parameter, which is used to get an access token and fetch the user’s profile information. See the explanation here for a worked example. You can either:\n\nUse client.retr_info(code) to get all the profile information, or\nUse client.retr_id(code) to get just the user’s ID.\n\nAfter either of these calls, you can also access the access token (used to revoke access, for example) with client.token[\"access_token\"].\n\nsource\n\n\nurl_match\n\n url_match (url, patterns=('^(localhost|127\\\\.0\\\\.0\\\\.1)(:\\\\d+)?$',))\n\n\nsource\n\n\nOAuth\n\n OAuth (app, cli, skip=None, redir_path='/redirect',\n logout_path='/logout', login_path='/login', https=True,\n http_patterns=('^(localhost|127\\\\.0\\\\.0\\\\.1)(:\\\\d+)?$',))\n\nInitialize self. See help(type(self)) for accurate signature.",
+ "crumbs": [
+ "Home",
+ "Source",
+ "OAuth"
+ ]
+ },
+ {
+ "objectID": "index.html",
+ "href": "index.html",
+ "title": "FastHTML",
+ "section": "",
+ "text": "Welcome to the official FastHTML documentation.\nFastHTML is a new next-generation web framework for fast, scalable web applications with minimal, compact code. It’s designed to be:\nFastHTML apps are just Python code, so you can use FastHTML with the full power of the Python language and ecosystem. FastHTML’s functionality maps 1:1 directly to HTML and HTTP, but allows them to be encapsulated using good software engineering practices—so you’ll need to understand these foundations to use this library fully. To understand how and why this works, please read this first: about.fastht.ml.",
+ "crumbs": [
+ "Home",
+ "Get Started"
+ ]
+ },
+ {
+ "objectID": "index.html#installation",
+ "href": "index.html#installation",
+ "title": "FastHTML",
+ "section": "Installation",
+ "text": "Installation\nSince fasthtml is a Python library, you can install it with:\npip install python-fasthtml\nIn the near future, we hope to add component libraries that can likewise be installed via pip.",
+ "crumbs": [
+ "Home",
+ "Get Started"
+ ]
+ },
+ {
+ "objectID": "index.html#usage",
+ "href": "index.html#usage",
+ "title": "FastHTML",
+ "section": "Usage",
+ "text": "Usage\nFor a minimal app, create a file “main.py” as follows:\n\n\nmain.py\n\nfrom fasthtml.common import *\n\napp,rt = fast_app()\n\n@rt('/')\ndef get(): return Div(P('Hello World!'), hx_get=\"/change\")\n\nserve()\n\nRunning the app with python main.py prints out a link to your running app: http://localhost:5001. Visit that link in your browser and you should see a page with the text “Hello World!”. Congratulations, you’ve just created your first FastHTML app!\nAdding interactivity is surprisingly easy, thanks to HTMX. Modify the file to add this function:\n\n\nmain.py\n\n@rt('/change')\ndef get(): return P('Nice to be here!')\n\nYou now have a page with a clickable element that changes the text when clicked. When clicking on this link, the server will respond with an “HTML partial”—that is, just a snippet of HTML which will be inserted into the existing page. In this case, the returned element will replace the original P element (since that’s the default behavior of HTMX) with the new version returned by the second route.\nThis “hypermedia-based” approach to web development is a powerful way to build web applications.\n\nGetting help from AI\nBecause FastHTML is newer than most LLMs, AI systems like Cursor, ChatGPT, Claude, and Copilot won’t give useful answers about it. To fix that problem, we’ve provided an LLM-friendly guide that teaches them how to use FastHTML. To use it, add this link for your AI helper to use:\n\n/llms-ctx.txt\n\nThis example is in a format based on recommendations from Anthropic for use with Claude Projects. This works so well that we’ve actually found that Claude can provide even better information than our own documentation! For instance, read through this annotated Claude chat for some great getting-started information, entirely generated from a project using the above text file as context.\nIf you use Cursor, type @doc then choose “Add new doc”, and use the /llms-ctx.txt link above. The context file is auto-generated from our llms.txt (our proposed standard for providing AI-friendly information)—you can generate alternative versions suitable for other models as needed.",
+ "crumbs": [
+ "Home",
+ "Get Started"
+ ]
+ },
+ {
+ "objectID": "index.html#next-steps",
+ "href": "index.html#next-steps",
+ "title": "FastHTML",
+ "section": "Next Steps",
+ "text": "Next Steps\nStart with the official sources to learn more about FastHTML:\n\nAbout: Learn about the core ideas behind FastHTML\nDocumentation: Learn from examples how to write FastHTML code\nIdiomatic app: Heavily commented source code walking through a complete application, including custom authentication, JS library connections, and database use.\n\nWe also have a 1-hour intro video:\n\nThe capabilities of FastHTML are vast and growing, and not all the features and patterns have been documented yet. Be prepared to invest time into studying and modifying source code, such as the main FastHTML repo’s notebooks and the official FastHTML examples repo:\n\nFastHTML Examples Repo on GitHub\nFastHTML Repo on GitHub\n\nThen explore the small but growing third-party ecosystem of FastHTML tutorials, notebooks, libraries, and components:\n\nFastHTML Gallery: Learn from minimal examples of components (ie chat bubbles, click-to-edit, infinite scroll, etc)\nCreating Custom FastHTML Tags for Markdown Rendering by Isaac Flath\nYour tutorial here!\n\nFinally, join the FastHTML community to ask questions, share your work, and learn from others:\n\nDiscord",
+ "crumbs": [
+ "Home",
+ "Get Started"
+ ]
+ },
+ {
+ "objectID": "tutorials/jupyter_and_fasthtml.html",
+ "href": "tutorials/jupyter_and_fasthtml.html",
+ "title": "Using Jupyter to write FastHTML",
+ "section": "",
+ "text": "Writing FastHTML applications in Jupyter notebooks requires a slightly different process than normal Python applications.\n\n\n\n\n\n\nDownload this notebook and try it yourself\n\n\n\nThe source code for this page is a Jupyter notebook. That makes it easy to directly experiment with it. However, as this is working code that means we have to comment out a few things in order for the documentation to build.\n\n\nThe first step is to import necessary libraries. As using FastHTML inside a Jupyter notebook is a special case, it remains a special import.\n\nfrom fasthtml.common import *\nfrom fasthtml.jupyter import *\n\nLet’s use jupy_app to instantiate app and rt objects. jupy_app is a thin wrapper around the fast_app function.\n\napp, rt = jupy_app(pico=True)\n\nDefine a route to test the application.\n\n@rt\ndef index():\n return Titled('Hello, Jupyter',\n P('Welcome to the FastHTML + Jupyter example'),\n Button('Click', hx_get='/click', hx_target='#dest'),\n Div(id='dest')\n )\n\nCreate a server object using JupyUvi, which also starts Uvicorn. The server runs in a separate thread from Jupyter, so it can use normal HTTP client functions in a notebook.\n\nserver = JupyUvi(app)\n\nThe HTMX callable displays the server’s HTMX application in an iframe which can be displayed by Jupyter notebook. Pass in the same port variable used in the JupyUvi callable above.\n\n# This doesn't display in the docs - uncomment and run it to see it in action\n# HTMX()\n\nWe didn’t define the /click route, but that’s fine - we can define (or change) it any time, and it’s dynamically inserted into the running app. No need to restart or reload anything!\n\n@rt\ndef click(): return P('You clicked me!')\n\nWhen you want to gracefully shut down the server use the server.stop() function displayed below. If you restart Jupyter without calling this line the thread may not be released and the HTMX callable above may throw errors. If that happens, a quick temporary fix is to change the port number above to something else.\nCleaner solutions to the dangling thread are to kill the dangling thread (dependant on each operating system) or restart the computer.\n\nserver.stop()",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "Using Jupyter to write FastHTML"
+ ]
+ },
+ {
+ "objectID": "tutorials/by_example.html",
+ "href": "tutorials/by_example.html",
+ "title": "FastHTML By Example",
+ "section": "",
+ "text": "This tutorial provides an alternate introduction to FastHTML by building out example applications. We also illustrate how to use FastHTML foundations to create custom web apps. Finally, this document serves as minimal context for a LLM to turn it into a FastHTML assistant.\nLet’s get started.",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "FastHTML By Example"
+ ]
+ },
+ {
+ "objectID": "tutorials/by_example.html#fasthtml-basics",
+ "href": "tutorials/by_example.html#fasthtml-basics",
+ "title": "FastHTML By Example",
+ "section": "FastHTML Basics",
+ "text": "FastHTML Basics\nFastHTML is just Python. You can install it with pip install python-fasthtml. Extensions/components built for it can likewise be distributed via PyPI or as simple Python files.\nThe core usage of FastHTML is to define routes, and then to define what to do at each route. This is similar to the FastAPI web framework (in fact we implemented much of the functionality to match the FastAPI usage examples), but where FastAPI focuses on returning JSON data to build APIs, FastHTML focuses on returning HTML data.\nHere’s a simple FastHTML app that returns a “Hello, World” message:\n\nfrom fasthtml.common import FastHTML, serve\n\napp = FastHTML()\n\n@app.get(\"/\")\ndef home():\n return \"<h1>Hello, World</h1>\"\n\nserve()\n\nTo run this app, place it in a file, say app.py, and then run it with python app.py.\nINFO: Will watch for changes in these directories: ['/home/jonathan/fasthtml-example']\nINFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)\nINFO: Started reloader process [871942] using WatchFiles\nINFO: Started server process [871945]\nINFO: Waiting for application startup.\nINFO: Application startup complete.\nIf you navigate to http://127.0.0.1:8000 in a browser, you’ll see your “Hello, World”. If you edit the app.py file and save it, the server will reload and you’ll see the updated message when you refresh the page in your browser.",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "FastHTML By Example"
+ ]
+ },
+ {
+ "objectID": "tutorials/by_example.html#constructing-html",
+ "href": "tutorials/by_example.html#constructing-html",
+ "title": "FastHTML By Example",
+ "section": "Constructing HTML",
+ "text": "Constructing HTML\nNotice we wrote some HTML in the previous example. We don’t want to do that! Some web frameworks require that you learn HTML, CSS, JavaScript AND some templating language AND python. We want to do as much as possible with just one language. Fortunately, the Python module fastcore.xml has all we need for constructing HTML from Python, and FastHTML includes all the tags you need to get started. For example:\n\nfrom fasthtml.common import *\npage = Html(\n Head(Title('Some page')),\n Body(Div('Some text, ', A('A link', href='https://example.com'), Img(src=\"https://placehold.co/200\"), cls='myclass')))\nprint(to_xml(page))\n\n<!doctype html></!doctype>\n\n<html>\n <head>\n <title>Some page</title>\n </head>\n <body>\n <div class=\"myclass\">\nSome text, \n <a href=\"https://example.com\">A link</a>\n <img src=\"https://placehold.co/200\">\n </div>\n </body>\n</html>\n\n\n\n\nshow(page)\n\n\n\n\n \n Some page\n \n \n \nSome text, \n A link\n \n \n \n\n\n\nIf that import * worries you, you can always import only the tags you need.\nFastHTML is smart enough to know about fastcore.xml, and so you don’t need to use the to_xml function to convert your FT objects to HTML. You can just return them as you would any other Python object. For example, if we modify our previous example to use fastcore.xml, we can return an FT object directly:\n\nfrom fasthtml.common import *\napp = FastHTML()\n\n@app.get(\"/\")\ndef home():\n page = Html(\n Head(Title('Some page')),\n Body(Div('Some text, ', A('A link', href='https://example.com'), Img(src=\"https://placehold.co/200\"), cls='myclass')))\n return page\n\nserve()\n\nThis will render the HTML in the browser.\nFor debugging, you can right-click on the rendered HTML in the browser and select “Inspect” to see the underlying HTML that was generated. There you’ll also find the ‘network’ tab, which shows you the requests that were made to render the page. Refresh and look for the request to 127.0.0.1 - and you’ll see it’s just a GET request to /, and the response body is the HTML you just returned.\n\n\n\n\n\n\nLive Reloading\n\n\n\nYou can also enable live reloading so you don’t have to manually refresh your browser to view updates.\n\n\nYou can also use Starlette’s TestClient to try it out in a notebook:\n\nfrom starlette.testclient import TestClient\nclient = TestClient(app)\nr = client.get(\"/\")\nprint(r.text)\n\n<html>\n <head><title>Some page</title>\n</head>\n <body><div class=\"myclass\">\nSome text, \n <a href=\"https://example.com\">A link</a>\n <img src=\"https://placehold.co/200\">\n</div>\n</body>\n</html>\n\n\n\nFastHTML wraps things in an Html tag if you don’t do it yourself (unless the request comes from htmx, in which case you get the element directly). See FT objects and HTML for more on creating custom components or adding HTML rendering to existing Python objects. To give the page a non-default title, return a Title before your main content:\n\napp = FastHTML()\n\n@app.get(\"/\")\ndef home():\n return Title(\"Page Demo\"), Div(H1('Hello, World'), P('Some text'), P('Some more text'))\n\nclient = TestClient(app)\nprint(client.get(\"/\").text)\n\n<!doctype html></!doctype>\n\n<html>\n <head>\n <title>Page Demo</title>\n <meta charset=\"utf-8\"></meta>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\"></meta>\n <script src=\"https://unpkg.com/htmx.org@next/dist/htmx.min.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/gh/answerdotai/surreal@1.3.0/surreal.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js\"></script>\n </head>\n <body>\n<div>\n <h1>Hello, World</h1>\n <p>Some text</p>\n <p>Some more text</p>\n</div>\n </body>\n</html>\n\n\n\nWe’ll use this pattern often in the examples to follow.",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "FastHTML By Example"
+ ]
+ },
+ {
+ "objectID": "tutorials/by_example.html#defining-routes",
+ "href": "tutorials/by_example.html#defining-routes",
+ "title": "FastHTML By Example",
+ "section": "Defining Routes",
+ "text": "Defining Routes\nThe HTTP protocol defines a number of methods (‘verbs’) to send requests to a server. The most common are GET, POST, PUT, DELETE, and HEAD. We saw ‘GET’ in action before - when you navigate to a URL, you’re making a GET request to that URL. We can do different things on a route for different HTTP methods. For example:\n@app.route(\"/\", methods='get')\ndef home():\n return H1('Hello, World')\n\n@app.route(\"/\", methods=['post', 'put'])\ndef post_or_put():\n return \"got a POST or PUT request\"\nThis says that when someone navigates to the root URL “/” (i.e. sends a GET request), they will see the big “Hello, World” heading. When someone submits a POST or PUT request to the same URL, the server should return the string “got a post or put request”.\n\n\n\n\n\n\nTest the POST request\n\n\n\nYou can test the POST request with curl -X POST http://127.0.0.1:8000 -d \"some data\". This sends some data to the server, you should see the response “got a post or put request” printed in the terminal.\n\n\nThere are a few other ways you can specify the route+method - FastHTML has .get, .post, etc. as shorthand for route(..., methods=['get']), etc.\n\n@app.get(\"/\")\ndef my_function():\n return \"Hello World from a GET request\"\n\nOr you can use the @rt decorator without a method but specify the method with the name of the function. For example:\n\nrt = app.route\n\n@rt(\"/\")\ndef post():\n return \"Hello World from a POST request\"\n\n\nclient.post(\"/\").text\n\n'Hello World from a POST request'\n\n\nYou’re welcome to pick whichever style you prefer. Using routes lets you show different content on different pages - ‘/home’, ‘/about’ and so on. You can also respond differently to different kinds of requests to the same route, as shown above. You can also pass data via the route:\n\n@app.get@rt\n\n\n\n@app.get(\"/greet/{nm}\")\ndef greet(nm:str):\n return f\"Good day to you, {nm}!\"\n\nclient.get(\"/greet/Dave\").text\n\n'Good day to you, Dave!'\n\n\n\n\n\n@rt(\"/greet/{nm}\")\ndef get(nm:str):\n return f\"Good day to you, {nm}!\"\n\nclient.get(\"/greet/Dave\").text\n\n'Good day to you, Dave!'\n\n\n\n\n\nMore on this in the More on Routing and Request Parameters section, which goes deeper into the different ways to get information from a request.",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "FastHTML By Example"
+ ]
+ },
+ {
+ "objectID": "tutorials/by_example.html#styling-basics",
+ "href": "tutorials/by_example.html#styling-basics",
+ "title": "FastHTML By Example",
+ "section": "Styling Basics",
+ "text": "Styling Basics\nPlain HTML probably isn’t quite what you imagine when you visualize your beautiful web app. CSS is the go-to language for styling HTML. But again, we don’t want to learn extra languages unless we absolutely have to! Fortunately, there are ways to get much more visually appealing sites by relying on the hard work of others, using existing CSS libraries. One of our favourites is PicoCSS. A common way to add CSS files to web pages is to use a <link> tag inside your HTML header, like this:\n<header>\n ...\n <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.min.css\">\n</header>\nFor convenience, FastHTML already defines a Pico component for you with picolink:\n\nprint(to_xml(picolink))\n\n<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.min.css\">\n\n<style>:root { --pico-font-size: 100%; }</style>\n\n\n\n\n\n\n\n\n\nNote\n\n\n\npicolink also includes a <style> tag, as we found that setting the font-size to 100% to be a good default. We show you how to override this below.\n\n\nSince we typically want CSS styling on all pages of our app, FastHTML lets you define a shared HTML header with the hdrs argument as shown below:\n\nfrom fasthtml.common import *\n1css = Style(':root {--pico-font-size:90%,--pico-font-family: Pacifico, cursive;}')\n2app = FastHTML(hdrs=(picolink, css))\n\n@app.route(\"/\")\ndef get():\n return (Title(\"Hello World\"), \n3 Main(H1('Hello, World'), cls=\"container\"))\n\n\n1\n\nCustom styling to override the pico defaults\n\n2\n\nDefine shared headers for all pages\n\n3\n\nAs per the pico docs, we put all of our content inside a <main> tag with a class of container:\n\n\n\n\n\n\n\n\n\n\nReturning Tuples\n\n\n\nWe’re returning a tuple here (a title and the main page). Returning a tuple, list, FT object, or an object with a __ft__ method tells FastHTML to turn the main body into a full HTML page that includes the headers (including the pico link and our custom css) which we passed in. This only occurs if the request isn’t from HTMX (for HTMX requests we need only return the rendered components).\n\n\nYou can check out the Pico examples page to see how different elements will look. If everything is working, the page should now render nice text with our custom font, and it should respect the user’s light/dark mode preferences too.\nIf you want to override the default styles or add more custom CSS, you can do so by adding a <style> tag to the headers as shown above. So you are allowed to write CSS to your heart’s content - we just want to make sure you don’t necessarily have to! Later on we’ll see examples using other component libraries and tailwind css to do more fancy styling things, along with tips to get an LLM to write all those fiddly bits so you don’t have to.",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "FastHTML By Example"
+ ]
+ },
+ {
+ "objectID": "tutorials/by_example.html#web-page---web-app",
+ "href": "tutorials/by_example.html#web-page---web-app",
+ "title": "FastHTML By Example",
+ "section": "Web Page -> Web App",
+ "text": "Web Page -> Web App\nShowing content is all well and good, but we typically expect a bit more interactivity from something calling itself a web app! So, let’s add a few different pages, and use a form to let users add messages to a list:\n\napp = FastHTML()\nmessages = [\"This is a message, which will get rendered as a paragraph\"]\n\n@app.get(\"/\")\ndef home():\n return Main(H1('Messages'), \n *[P(msg) for msg in messages],\n A(\"Link to Page 2 (to add messages)\", href=\"/page2\"))\n\n@app.get(\"/page2\")\ndef page2():\n return Main(P(\"Add a message with the form below:\"),\n Form(Input(type=\"text\", name=\"data\"),\n Button(\"Submit\"),\n action=\"/\", method=\"post\"))\n\n@app.post(\"/\")\ndef add_message(data:str):\n messages.append(data)\n return home()\n\nWe re-render the entire homepage to show the newly added message. This is fine, but modern web apps often don’t re-render the entire page, they just update a part of the page. In fact even very complicated applications are often implemented as ‘Single Page Apps’ (SPAs). This is where HTMX comes in.",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "FastHTML By Example"
+ ]
+ },
+ {
+ "objectID": "tutorials/by_example.html#htmx",
+ "href": "tutorials/by_example.html#htmx",
+ "title": "FastHTML By Example",
+ "section": "HTMX",
+ "text": "HTMX\nHTMX addresses some key limitations of HTML. In vanilla HTML, links can trigger a GET request to show a new page, and forms can send requests containing data to the server. A lot of ‘Web 1.0’ design revolved around ways to use these to do everything we wanted. But why should only some elements be allowed to trigger requests? And why should we refresh the entire page with the result each time one does? HTMX extends HTML to allow us to trigger requests from any element on all kinds of events, and to update a part of the page without refreshing the entire page. It’s a powerful tool for building modern web apps.\nIt does this by adding attributes to HTML tags to make them do things. For example, here’s a page with a counter and a button that increments it:\n\napp = FastHTML()\n\ncount = 0\n\n@app.get(\"/\")\ndef home():\n return Title(\"Count Demo\"), Main(\n H1(\"Count Demo\"),\n P(f\"Count is set to {count}\", id=\"count\"),\n Button(\"Increment\", hx_post=\"/increment\", hx_target=\"#count\", hx_swap=\"innerHTML\")\n )\n\n@app.post(\"/increment\")\ndef increment():\n print(\"incrementing\")\n global count\n count += 1\n return f\"Count is set to {count}\"\n\nThe button triggers a POST request to /increment (since we set hx_post=\"/increment\"), which increments the count and returns the new count. The hx_target attribute tells HTMX where to put the result. If no target is specified it replaces the element that triggered the request. The hx_swap attribute specifies how it adds the result to the page. Useful options are:\n\ninnerHTML: Replace the target element’s content with the result.\nouterHTML: Replace the target element with the result.\nbeforebegin: Insert the result before the target element.\nbeforeend: Insert the result inside the target element, after its last child.\nafterbegin: Insert the result inside the target element, before its first child.\nafterend: Insert the result after the target element.\n\nYou can also use an hx_swap of delete to delete the target element regardless of response, or of none to do nothing.\nBy default, requests are triggered by the “natural” event of an element - click in the case of a button (and most other elements). You can also specify different triggers, along with various modifiers - see the HTMX docs for more.\nThis pattern of having elements trigger requests that modify or replace other elements is a key part of the HTMX philosophy. It takes a little getting used to, but once mastered it is extremely powerful.\n\nReplacing Elements Besides the Target\nSometimes having a single target is not enough, and we’d like to specify some additional elements to update or remove. In these cases, returning elements with an id that matches the element to be replaced and hx_swap_oob='true' will replace those elements too. We’ll use this in the next example to clear an input field when we submit a form.",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "FastHTML By Example"
+ ]
+ },
+ {
+ "objectID": "tutorials/by_example.html#full-example-1---todo-app",
+ "href": "tutorials/by_example.html#full-example-1---todo-app",
+ "title": "FastHTML By Example",
+ "section": "Full Example #1 - ToDo App",
+ "text": "Full Example #1 - ToDo App\nThe canonical demo web app! A TODO list. Rather than create yet another variant for this tutorial, we recommend starting with this video tutorial from Jeremy:\n\n\n\n\nimage.png\n\n\nWe’ve made a number of variants of this app - so in addition to the version shown in the video you can browse this series of examples with increasing complexity, the heavily-commented “idiomatic” version here, and the example linked from the FastHTML homepage.",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "FastHTML By Example"
+ ]
+ },
+ {
+ "objectID": "tutorials/by_example.html#full-example-2---image-generation-app",
+ "href": "tutorials/by_example.html#full-example-2---image-generation-app",
+ "title": "FastHTML By Example",
+ "section": "Full Example #2 - Image Generation App",
+ "text": "Full Example #2 - Image Generation App\nLet’s create an image generation app. We’d like to wrap a text-to-image model in a nice UI, where the user can type in a prompt and see a generated image appear. We’ll use a model hosted by Replicate to actually generate the images. Let’s start with the homepage, with a form to submit prompts and a div to hold the generated images:\n# Main page\n@app.get(\"/\")\ndef get():\n inp = Input(id=\"new-prompt\", name=\"prompt\", placeholder=\"Enter a prompt\")\n add = Form(Group(inp, Button(\"Generate\")), hx_post=\"/\", target_id='gen-list', hx_swap=\"afterbegin\")\n gen_list = Div(id='gen-list')\n return Title('Image Generation Demo'), Main(H1('Magic Image Generation'), add, gen_list, cls='container')\nSubmitting the form will trigger a POST request to /, so next we need to generate an image and add it to the list. One problem: generating images is slow! We’ll start the generation in a separate thread, but this now surfaces a different problem: we want to update the UI right away, but our image will only be ready a few seconds later. This is a common pattern - think about how often you see a loading spinner online. We need a way to return a temporary bit of UI which will eventually be replaced by the final image. Here’s how we might do this:\ndef generation_preview(id):\n if os.path.exists(f\"gens/{id}.png\"):\n return Div(Img(src=f\"/gens/{id}.png\"), id=f'gen-{id}')\n else:\n return Div(\"Generating...\", id=f'gen-{id}', \n hx_post=f\"/generations/{id}\",\n hx_trigger='every 1s', hx_swap='outerHTML')\n \n@app.post(\"/generations/{id}\")\ndef get(id:int): return generation_preview(id)\n\n@app.post(\"/\")\ndef post(prompt:str):\n id = len(generations)\n generate_and_save(prompt, id)\n generations.append(prompt)\n clear_input = Input(id=\"new-prompt\", name=\"prompt\", placeholder=\"Enter a prompt\", hx_swap_oob='true')\n return generation_preview(id), clear_input\n\n@threaded\ndef generate_and_save(prompt, id): ... \nThe form sends the prompt to the / route, which starts the generation in a separate thread then returns two things:\n\nA generation preview element that will be added to the top of the gen-list div (since that is the target_id of the form which triggered the request)\nAn input field that will replace the form’s input field (that has the same id), using the hx_swap_oob=‘true’ trick. This clears the prompt field so the user can type another prompt.\n\nThe generation preview first returns a temporary “Generating…” message, which polls the /generations/{id} route every second. This is done by setting hx_post to the route and hx_trigger to ‘every 1s’. The /generations/{id} route returns the preview element every second until the image is ready, at which point it returns the final image. Since the final image replaces the temporary one (hx_swap=‘outerHTML’), the polling stops running and the generation preview is now complete.\nThis works nicely - the user can submit several prompts without having to wait for the first one to generate, and as the images become available they are added to the list. You can see the full code of this version here.\n\nAgain, with Style\nThe app is functional, but can be improved. The next version adds more stylish generation previews, lays out the images in a grid layout that is responsive to different screen sizes, and adds a database to track generations and make them persistent. The database part is very similar to the todo list example, so let’s just quickly look at how we add the nice grid layout. This is what the result looks like:\n\n\n\nimage.png\n\n\nStep one was looking around for existing components. The Pico CSS library we’ve been using has a rudimentary grid but recommends using an alternative layout system. One of the options listed was Flexbox.\nTo use Flexbox you create a “row” with one or more elements. You can specify how wide things should be with a specific syntax in the class name. For example, col-xs-12 means a box that will take up 12 columns (out of 12 total) of the row on extra small screens, col-sm-6 means a column that will take up 6 columns of the row on small screens, and so on. So if you want four columns on large screens you would use col-lg-3 for each item (i.e. each item is using 3 columns out of 12).\n<div class=\"row\">\n <div class=\"col-xs-12\">\n <div class=\"box\">This takes up the full width</div>\n </div>\n</div>\nThis was non-intuitive to me. Thankfully ChatGPT et al know web stuff quite well, and we can also experiment in a notebook to test things out:\n\ngrid = Html(\n Link(rel=\"stylesheet\", href=\"https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css\", type=\"text/css\"),\n Div(\n Div(Div(\"This takes up the full width\", cls=\"box\", style=\"background-color: #800000;\"), cls=\"col-xs-12\"),\n Div(Div(\"This takes up half\", cls=\"box\", style=\"background-color: #008000;\"), cls=\"col-xs-6\"),\n Div(Div(\"This takes up half\", cls=\"box\", style=\"background-color: #0000B0;\"), cls=\"col-xs-6\"),\n cls=\"row\", style=\"color: #fff;\"\n )\n)\nshow(grid)\n\n\n\n\n \n \n \n This takes up the full width\n \n \n This takes up half\n \n \n This takes up half\n \n \n\n\n\nAside: when in doubt with CSS stuff, add a background color or a border so you can see what’s happening!\nTranslating this into our app, we have a new homepage with a div (class=\"row\") to store the generated images / previews, and a generation_preview function that returns boxes with the appropriate classes and styles to make them appear in the grid. I chose a layout with different numbers of columns for different screen sizes, but you could also just specify the col-xs class if you wanted the same layout on all devices.\ngridlink = Link(rel=\"stylesheet\", href=\"https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css\", type=\"text/css\")\napp = FastHTML(hdrs=(picolink, gridlink))\n\n# Main page\n@app.get(\"/\")\ndef get():\n inp = Input(id=\"new-prompt\", name=\"prompt\", placeholder=\"Enter a prompt\")\n add = Form(Group(inp, Button(\"Generate\")), hx_post=\"/\", target_id='gen-list', hx_swap=\"afterbegin\")\n gen_containers = [generation_preview(g) for g in gens(limit=10)] # Start with last 10\n gen_list = Div(*gen_containers[::-1], id='gen-list', cls=\"row\") # flexbox container: class = row\n return Title('Image Generation Demo'), Main(H1('Magic Image Generation'), add, gen_list, cls='container')\n\n# Show the image (if available) and prompt for a generation\ndef generation_preview(g):\n grid_cls = \"box col-xs-12 col-sm-6 col-md-4 col-lg-3\"\n image_path = f\"{g.folder}/{g.id}.png\"\n if os.path.exists(image_path):\n return Div(Card(\n Img(src=image_path, alt=\"Card image\", cls=\"card-img-top\"),\n Div(P(B(\"Prompt: \"), g.prompt, cls=\"card-text\"),cls=\"card-body\"),\n ), id=f'gen-{g.id}', cls=grid_cls)\n return Div(f\"Generating gen {g.id} with prompt {g.prompt}\", \n id=f'gen-{g.id}', hx_get=f\"/gens/{g.id}\", \n hx_trigger=\"every 2s\", hx_swap=\"outerHTML\", cls=grid_cls)\nYou can see the final result in main.py in the image_app_simple example directory, along with info on deploying it (tl;dr don’t!). We’ve also deployed a version that only shows your generations (tied to browser session) and has a credit system to save our bank accounts. You can access that here. Now for the next question: how do we keep track of different users?\n\n\nAgain, with Sessions\nAt the moment everyone sees all images! How do we keep some sort of unique identifier tied to a user? Before going all the way to setting up users, login pages etc., let’s look at a way to at least limit generations to the user’s session. You could do this manually with cookies. For convenience and security, fasthtml (via Starlette) has a special mechanism for storing small amounts of data in the user’s browser via the session argument to your route. This acts like a dictionary and you can set and get values from it. For example, here we look for a session_id key, and if it doesn’t exist we generate a new one:\n@app.get(\"/\")\ndef get(session):\n if 'session_id' not in session: session['session_id'] = str(uuid.uuid4())\n return H1(f\"Session ID: {session['session_id']}\")\nRefresh the page a few times - you’ll notice that the session ID remains the same. If you clear your browsing data, you’ll get a new session ID. And if you load the page in a different browser (but not a different tab), you’ll get a new session ID. This will persist within the current browser, letting us use it as a key for our generations. As a bonus, someone can’t spoof this session id by passing it in another way (for example, sending a query parameter). Behind the scenes, the data is stored in a browser cookie but it is signed with a secret key that stops the user or anyone nefarious from being able to tamper with it. The cookie is decoded back into a dictionary by something called a middleware function, which we won’t cover here. All you need to know is that we can use this to store bits of state in the user’s browser.\nIn the image app example, we can add a session_id column to our database, and modify our homepage like so:\n@app.get(\"/\")\ndef get(session):\n if 'session_id' not in session: session['session_id'] = str(uuid.uuid4())\n inp = Input(id=\"new-prompt\", name=\"prompt\", placeholder=\"Enter a prompt\")\n add = Form(Group(inp, Button(\"Generate\")), hx_post=\"/\", target_id='gen-list', hx_swap=\"afterbegin\")\n gen_containers = [generation_preview(g) for g in gens(limit=10, where=f\"session_id == '{session['session_id']}'\")]\n ...\nSo we check if the session id exists in the session, add one if not, and then limit the generations shown to only those tied to this session id. We filter the database with a where clause - see [TODO link Jeremy’s example for a more reliable way to do this]. The only other change we need to make is to store the session id in the database when a generation is made. You can check out this version here. You could instead write this app without relying on a database at all - simply storing the filenames of the generated images in the session, for example. But this more general approach of linking some kind of unique session identifier to users or data in our tables is a useful general pattern for more complex examples.\n\n\nAgain, with Credits!\nGenerating images with replicate costs money. So next let’s add a pool of credits that get used up whenever anyone generates an image. To recover our lost funds, we’ll also set up a payment system so that generous users can buy more credits for everyone. You could modify this to let users buy credits tied to their session ID, but at that point you risk having angry customers losing their money after wiping their browser history, and should consider setting up proper account management :)\nTaking payments with Stripe is intimidating but very doable. Here’s a tutorial that shows the general principle using Flask. As with other popular tasks in the web-dev world, ChatGPT knows a lot about Stripe - but you should exercise extra caution when writing code that handles money!\nFor the finished example we add the bare minimum:\n\nA way to create a Stripe checkout session and redirect the user to the session URL\n‘Success’ and ‘Cancel’ routes to handle the result of the checkout\nA route that listens for a webhook from Stripe to update the number of credits when a payment is made.\n\nIn a typical application you’ll want to keep track of which users make payments, catch other kinds of stripe events and so on. This example is more a ‘this is possible, do your own research’ than ‘this is how you do it’. But hopefully it does illustrate the key idea: there is no magic here. Stripe (and many other technologies) relies on sending users to different routes and shuttling data back and forth in requests. And we know how to do that!",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "FastHTML By Example"
+ ]
+ },
+ {
+ "objectID": "tutorials/by_example.html#more-on-routing-and-request-parameters",
+ "href": "tutorials/by_example.html#more-on-routing-and-request-parameters",
+ "title": "FastHTML By Example",
+ "section": "More on Routing and Request Parameters",
+ "text": "More on Routing and Request Parameters\nThere are a number of ways information can be passed to the server. When you specify arguments to a route, FastHTML will search the request for values with the same name, and convert them to the correct type. In order, it searches\n\nThe path parameters\nThe query parameters\nThe cookies\nThe headers\nThe session\nForm data\n\nThere are also a few special arguments\n\nrequest (or any prefix like req): gets the raw Starlette Request object\nsession (or any prefix like sess): gets the session object\nauth\nhtmx\napp\n\nIn this section let’s quickly look at some of these in action.\n\napp = FastHTML()\ncli = TestClient(app)\n\nPart of the route (path parameters):\n\n@app.get('/user/{nm}')\ndef _(nm:str): return f\"Good day to you, {nm}!\"\n\ncli.get('/user/jph').text\n\n'Good day to you, jph!'\n\n\nMatching with a regex:\n\nreg_re_param(\"imgext\", \"ico|gif|jpg|jpeg|webm\")\n\n@app.get(r'/static/{path:path}{fn}.{ext:imgext}')\ndef get_img(fn:str, path:str, ext:str): return f\"Getting {fn}.{ext} from /{path}\"\n\ncli.get('/static/foo/jph.ico').text\n\n'Getting jph.ico from /foo/'\n\n\nUsing an enum (try using a string that isn’t in the enum):\n\nModelName = str_enum('ModelName', \"alexnet\", \"resnet\", \"lenet\")\n\n@app.get(\"/models/{nm}\")\ndef model(nm:ModelName): return nm\n\nprint(cli.get('/models/alexnet').text)\n\nalexnet\n\n\nCasting to a Path:\n\n@app.get(\"/files/{path}\")\ndef txt(path: Path): return path.with_suffix('.txt')\n\nprint(cli.get('/files/foo').text)\n\nfoo.txt\n\n\nAn integer with a default value:\n\nfake_db = [{\"name\": \"Foo\"}, {\"name\": \"Bar\"}]\n\n@app.get(\"/items/\")\ndef read_item(idx:int|None = 0): return fake_db[idx]\n\nprint(cli.get('/items/?idx=1').text)\n\n{\"name\":\"Bar\"}\n\n\n\nprint(cli.get('/items/').text)\n\n{\"name\":\"Foo\"}\n\n\nBoolean values (takes anything “truthy” or “falsy”):\n\n@app.get(\"/booly/\")\ndef booly(coming:bool=True): return 'Coming' if coming else 'Not coming'\n\nprint(cli.get('/booly/?coming=true').text)\n\nComing\n\n\n\nprint(cli.get('/booly/?coming=no').text)\n\nNot coming\n\n\nGetting dates:\n\n@app.get(\"/datie/\")\ndef datie(d:parsed_date): return d\n\ndate_str = \"17th of May, 2024, 2p\"\nprint(cli.get(f'/datie/?d={date_str}').text)\n\n2024-05-17 14:00:00\n\n\nMatching a dataclass:\n\nfrom dataclasses import dataclass, asdict\n\n@dataclass\nclass Bodie:\n a:int;b:str\n\n@app.route(\"/bodie/{nm}\")\ndef post(nm:str, data:Bodie):\n res = asdict(data)\n res['nm'] = nm\n return res\n\ncli.post('/bodie/me', data=dict(a=1, b='foo')).text\n\n'{\"a\":1,\"b\":\"foo\",\"nm\":\"me\"}'\n\n\n\nCookies\nCookies can be set via a Starlette Response object, and can be read back by specifying the name:\n\nfrom datetime import datetime\n\n@app.get(\"/setcookie\")\ndef setc(req):\n now = datetime.now()\n res = Response(f'Set to {now}')\n res.set_cookie('now', str(now))\n return res\n\ncli.get('/setcookie').text\n\n'Set to 2024-07-20 23:14:54.364793'\n\n\n\n@app.get(\"/getcookie\")\ndef getc(now:parsed_date): return f'Cookie was set at time {now.time()}'\n\ncli.get('/getcookie').text\n\n'Cookie was set at time 23:14:54.364793'\n\n\n\n\nUser Agent and HX-Request\nAn argument of user_agent will match the header User-Agent. This holds for special headers like HX-Request (used by HTMX to signal when a request comes from an HTMX request) - the general pattern is that “-” is replaced with “_” and strings are turned to lowercase.\n\n@app.get(\"/ua\")\nasync def ua(user_agent:str): return user_agent\n\ncli.get('/ua', headers={'User-Agent':'FastHTML'}).text\n\n'FastHTML'\n\n\n\n@app.get(\"/hxtest\")\ndef hxtest(htmx): return htmx.request\n\ncli.get('/hxtest', headers={'HX-Request':'1'}).text\n\n'1'\n\n\n\n\nStarlette Requests\nIf you add an argument called request(or any prefix of that, for example req) it will be populated with the Starlette Request object. This is useful if you want to do your own processing manually. For example, although FastHTML will parse forms for you, you could instead get form data like so:\n@app.get(\"/form\")\nasync def form(request:Request):\n form_data = await request.form()\n a = form_data.get('a')\nSee the Starlette docs for more information on the Request object.\n\n\nStarlette Responses\nYou can return a Starlette Response object from a route to control the response. For example:\n@app.get(\"/redirect\")\ndef redirect():\n return RedirectResponse(url=\"/\")\nWe used this to set cookies in the previous example. See the Starlette docs for more information on the Response object.\n\n\nStatic Files\nWe often want to serve static files like images. This is easily done! For common file types (images, CSS etc) we can create a route that returns a Starlette FileResponse like so:\n# For images, CSS, etc.\n@app.get(\"/{fname:path}.{ext:static}\")\ndef static(fname: str, ext: str):\n return FileResponse(f'{fname}.{ext}')\nYou can customize it to suit your needs (for example, only serving files in a certain directory). You’ll notice some variant of this route in all our complete examples - even for apps with no static files the browser will typically request a /favicon.ico file, for example, and as the astute among you will have noticed this has sparked a bit of competition between Johno and Jeremy regarding which country flag should serve as the default!\n\n\nWebSockets\nFor certain applications such as multiplayer games, websockets can be a powerful feature. Luckily HTMX and FastHTML has you covered! Simply specify that you wish to include the websocket header extension from HTMX:\napp = FastHTML(ws_hdr=True)\nrt = app.route\nWith that, you are now able to specify the different websocket specific HTMX goodies. For example, say we have a website we want to setup a websocket, you can simply:\ndef mk_inp(): return Input(id='msg')\n\n@rt('/')\nasync def get(request):\n cts = Div(\n Div(id='notifications'),\n Form(mk_inp(), id='form', ws_send=True),\n hx_ext='ws', ws_connect='/ws')\n return Titled('Websocket Test', cts)\nAnd this will setup a connection on the route /ws along with a form that will send a message to the websocket whenever the form is submitted. Let’s go ahead and handle this route:\n@app.ws('/ws')\nasync def ws(msg:str, send):\n await send(Div('Hello ' + msg, id=\"notifications\"))\n await sleep(2)\n return Div('Goodbye ' + msg, id=\"notifications\"), mk_inp()\nOne thing you might have noticed is a lack of target id for our websocket trigger for swapping HTML content. This is because HTMX always swaps content with websockets with Out of Band Swaps. Therefore, HTMX will look for the id in the returned HTML content from the server for determining what to swap. To send stuff to the client, you can either use the send parameter or simply return the content or both!\nNow, sometimes you might want to perform actions when a client connects or disconnects such as add or remove a user from a player queue. To hook into these events, you can pass your connection or disconnection function to the app.ws decorator:\nasync def on_connect(send):\n print('Connected!')\n await send(Div('Hello, you have connected', id=\"notifications\"))\n\nasync def on_disconnect(ws):\n print('Disconnected!')\n\n@app.ws('/ws', conn=on_connect, disconn=on_disconnect)\nasync def ws(msg:str, send):\n await send(Div('Hello ' + msg, id=\"notifications\"))\n await sleep(2)\n return Div('Goodbye ' + msg, id=\"notifications\"), mk_inp()",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "FastHTML By Example"
+ ]
+ },
+ {
+ "objectID": "tutorials/by_example.html#full-example-3---chatbot-example-with-daisyui-components",
+ "href": "tutorials/by_example.html#full-example-3---chatbot-example-with-daisyui-components",
+ "title": "FastHTML By Example",
+ "section": "Full Example #3 - Chatbot Example with DaisyUI Components",
+ "text": "Full Example #3 - Chatbot Example with DaisyUI Components\nLet’s go back to the topic of adding components or styling beyond the simple PicoCSS examples so far. How might we adopt a component or framework? In this example, let’s build a chatbot UI leveraging the DaisyUI chat bubble. The final result will look like this:\n\n\n\nimage.png\n\n\nAt first glance, DaisyUI’s chat component looks quite intimidating. The examples look like this:\n<div class=\"chat chat-start\">\n <div class=\"chat-image avatar\">\n <div class=\"w-10 rounded-full\">\n <img alt=\"Tailwind CSS chat bubble component\" src=\"https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.jpg\" />\n </div>\n </div>\n <div class=\"chat-header\">\n Obi-Wan Kenobi\n <time class=\"text-xs opacity-50\">12:45</time>\n </div>\n <div class=\"chat-bubble\">You were the Chosen One!</div>\n <div class=\"chat-footer opacity-50\">\n Delivered\n </div>\n</div>\n<div class=\"chat chat-end\">\n <div class=\"chat-image avatar\">\n <div class=\"w-10 rounded-full\">\n <img alt=\"Tailwind CSS chat bubble component\" src=\"https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.jpg\" />\n </div>\n </div>\n <div class=\"chat-header\">\n Anakin\n <time class=\"text-xs opacity-50\">12:46</time>\n </div>\n <div class=\"chat-bubble\">I hate you!</div>\n <div class=\"chat-footer opacity-50\">\n Seen at 12:46\n </div>\n</div>\nWe have several things going for us however.\n\nChatGPT knows DaisyUI and Tailwind (DaisyUI is a Tailwind component library)\nWe can build things up piece by piece with AI standing by to help.\n\nhttps://h2f.answer.ai/ is a tool that can convert HTML to FT (fastcore.xml) and back, which is useful for getting a quick starting point when you have an HTML example to start from.\nWe can strip out some unnecessary bits and try to get the simplest possible example working in a notebook first:\n\n# Loading tailwind and daisyui\nheaders = (Script(src=\"https://cdn.tailwindcss.com\"),\n Link(rel=\"stylesheet\", href=\"https://cdn.jsdelivr.net/npm/daisyui@4.11.1/dist/full.min.css\"))\n\n# Displaying a single message\nd = Div(\n Div(\"Chat header here\", cls=\"chat-header\"),\n Div(\"My message goes here\", cls=\"chat-bubble chat-bubble-primary\"),\n cls=\"chat chat-start\"\n)\n# show(Html(*headers, d)) # uncomment to view\n\nNow we can extend this to render multiple messages, with the message being on the left (chat-start) or right (chat-end) depending on the role. While we’re at it, we can also change the color (chat-bubble-primary) of the message and put them all in a chat-box div:\n\nmessages = [\n {\"role\":\"user\", \"content\":\"Hello\"},\n {\"role\":\"assistant\", \"content\":\"Hi, how can I assist you?\"}\n]\n\ndef ChatMessage(msg):\n return Div(\n Div(msg['role'], cls=\"chat-header\"),\n Div(msg['content'], cls=f\"chat-bubble chat-bubble-{'primary' if msg['role'] == 'user' else 'secondary'}\"),\n cls=f\"chat chat-{'end' if msg['role'] == 'user' else 'start'}\")\n\nchatbox = Div(*[ChatMessage(msg) for msg in messages], cls=\"chat-box\", id=\"chatlist\")\n\n# show(Html(*headers, chatbox)) # Uncomment to view\n\nNext, it was back to the ChatGPT to tweak the chat box so it wouldn’t grow as messages were added. I asked:\n\"I have something like this (it's working now) \n[code]\nThe messages are added to this div so it grows over time. \nIs there a way I can set it's height to always be 80% of the total window height with a scroll bar if needed?\"\nBased on this query GPT4o helpfully shared that “This can be achieved using Tailwind CSS utility classes. Specifically, you can use h-[80vh] to set the height to 80% of the viewport height, and overflow-y-auto to add a vertical scroll bar when needed.”\nTo put it another way: none of the CSS classes in the following example were written by a human, and what edits I did make were informed by advice from the AI that made it relatively painless!\nThe actual chat functionality of the app is based on our claudette library. As with the image example, we face a potential hiccup in that getting a response from an LLM is slow. We need a way to have the user message added to the UI immediately, and then have the response added once it’s available. We could do something similar to the image generation example above, or use websockets. Check out the full example for implementations of both, along with further details.",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "FastHTML By Example"
+ ]
+ },
+ {
+ "objectID": "tutorials/by_example.html#full-example-4---multiplayer-game-of-life-example-with-websockets",
+ "href": "tutorials/by_example.html#full-example-4---multiplayer-game-of-life-example-with-websockets",
+ "title": "FastHTML By Example",
+ "section": "Full Example #4 - Multiplayer Game of Life Example with Websockets",
+ "text": "Full Example #4 - Multiplayer Game of Life Example with Websockets\nLet’s see how we can implement a collaborative website using Websockets in FastHTML. To showcase this, we will use the famous Conway’s Game of Life, which is a game that takes place in a grid world. Each cell in the grid can be either alive or dead. The cell’s state is initially given by a user before the game is started and then evolves through the iteration of the grid world once the clock starts. Whether a cell’s state will change from the previous state depends on simple rules based on its neighboring cells’ states. Here is the standard Game of Life logic implemented in Python courtesy of ChatGPT:\ngrid = [[0 for _ in range(20)] for _ in range(20)]\ndef update_grid(grid: list[list[int]]) -> list[list[int]]:\n new_grid = [[0 for _ in range(20)] for _ in range(20)]\n def count_neighbors(x, y):\n directions = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]\n count = 0\n for dx, dy in directions:\n nx, ny = x + dx, y + dy\n if 0 <= nx < len(grid) and 0 <= ny < len(grid[0]): count += grid[nx][ny]\n return count\n for i in range(len(grid)):\n for j in range(len(grid[0])):\n neighbors = count_neighbors(i, j)\n if grid[i][j] == 1:\n if neighbors < 2 or neighbors > 3: new_grid[i][j] = 0\n else: new_grid[i][j] = 1\n elif neighbors == 3: new_grid[i][j] = 1\n return new_grid\nThis would be a very dull game if we were to run it, since the initial state of everything would remain dead. Therefore, we need a way of letting the user give an initial state before starting the game. FastHTML to the rescue!\ndef Grid():\n cells = []\n for y, row in enumerate(game_state['grid']):\n for x, cell in enumerate(row):\n cell_class = 'alive' if cell else 'dead'\n cell = Div(cls=f'cell {cell_class}', hx_put='/update', hx_vals={'x': x, 'y': y}, hx_swap='none', hx_target='#gol', hx_trigger='click')\n cells.append(cell)\n return Div(*cells, id='grid')\n\n@rt('/update')\nasync def put(x: int, y: int):\n grid[y][x] = 1 if grid[y][x] == 0 else 0\nAbove is a component for representing the game’s state that the user can interact with and update on the server using cool HTMX features such as hx_vals for determining which cell was clicked to make it dead or alive. Now, you probably noticed that the HTTP request in this case is a PUT request, which does not return anything and this means our client’s view of the grid world and the server’s game state will immediately become out of sync :(. We could of course just return a new Grid component with the updated state, but that would only work for a single client, if we had more, they quickly get out of sync with each other and the server. Now Websockets to the rescue!\nWebsockets are a way for the server to keep a persistent connection with clients and send data to the client without explicitly being requested for information, which is not possible with HTTP. Luckily FastHTML and HTMX work well with Websockets. Simply state you wish to use websockets for your app and define a websocket route:\n...\napp = FastHTML(hdrs=(picolink, gridlink, css, htmx_ws), ws_hdr=True)\n\nplayer_queue = []\nasync def update_players():\n for i, player in enumerate(player_queue):\n try: await player(Grid())\n except: player_queue.pop(i)\nasync def on_connect(send): player_queue.append(send)\nasync def on_disconnect(send): await update_players()\n\n@app.ws('/gol', conn=on_connect, disconn=on_disconnect)\nasync def ws(msg:str, send): pass\n\ndef Home(): return Title('Game of Life'), Main(gol, Div(Grid(), id='gol', cls='row center-xs'), hx_ext=\"ws\", ws_connect=\"/gol\")\n\n@rt('/update')\nasync def put(x: int, y: int):\n grid[y][x] = 1 if grid[y][x] == 0 else 0\n await update_players()\n...\nHere we simply keep track of all the players that have connected or disconnected to our site and when an update occurs, we send updates to all the players still connected via websockets. Via HTMX, you are still simply exchanging HTML from the server to the client and will swap in the content based on how you setup your hx_swap attribute. There is only one difference, that being all swaps are OOB. You can find more information on the HTMX websocket extension documentation page here. You can find a full fledge hosted example of this app here.",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "FastHTML By Example"
+ ]
+ },
+ {
+ "objectID": "tutorials/by_example.html#ft-objects-and-html",
+ "href": "tutorials/by_example.html#ft-objects-and-html",
+ "title": "FastHTML By Example",
+ "section": "FT objects and HTML",
+ "text": "FT objects and HTML\nThese FT objects create a ‘FastTag’ structure [tag,children,attrs] for to_xml(). When we call Div(...), the elements we pass in are the children. Attributes are passed in as keywords. class and for are special words in python, so we use cls, klass or _class instead of class and fr or _for instead of for. Note these objects are just 3-element lists - you can create custom ones too as long as they’re also 3-element lists. Alternately, leaf nodes can be strings instead (which is why you can do Div('some text')). If you pass something that isn’t a 3-element list or a string, it will be converted to a string using str()… unless (our final trick) you define a __ft__ method that will run before str(), so you can render things a custom way.\nFor example, here’s one way we could make a custom class that can be rendered into HTML:\n\nclass Person:\n def __init__(self, name, age):\n self.name = name\n self.age = age\n\n def __ft__(self):\n return ['div', [f'{self.name} is {self.age} years old.'], {}]\n\np = Person('Jonathan', 28)\nprint(to_xml(Div(p, \"more text\", cls=\"container\")))\n\n<div class=\"container\">\n <div>Jonathan is 28 years old.</div>\nmore text\n</div>\n\n\n\nIn the examples, you’ll see we often patch in __ft__ methods to existing classes to control how they’re rendered. For example, if Person didn’t have a __ft__ method or we wanted to override it, we could add a new one like this:\n\nfrom fastcore.all import patch\n\n@patch\ndef __ft__(self:Person):\n return Div(\"Person info:\", Ul(Li(\"Name:\",self.name), Li(\"Age:\", self.age)))\n\nshow(p)\n\n\nPerson info:\n \n \nName:\nJonathan\n \n \nAge:\n28\n \n \n\n\n\nSome tags from fastcore.xml are overwritten by fasthtml.core and a few are further extended by fasthtml.xtend using this method. Over time, we hope to see others developing custom components too, giving us a larger and larger ecosystem of reusable components.",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "FastHTML By Example"
+ ]
+ },
+ {
+ "objectID": "tutorials/by_example.html#custom-scripts-and-styling",
+ "href": "tutorials/by_example.html#custom-scripts-and-styling",
+ "title": "FastHTML By Example",
+ "section": "Custom Scripts and Styling",
+ "text": "Custom Scripts and Styling\nThere are many popular JavaScript and CSS libraries that can be used via a simple Script or Style tag. But in some cases you will need to write more custom code. FastHTML’s js.py contains a few examples that may be useful as reference.\nFor example, to use the marked.js library to render markdown in a div, including in components added after the page has loaded via htmx, we do something like this:\nimport { marked } from \"https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js\";\nproc_htmx('%s', e => e.innerHTML = marked.parse(e.textContent));\nproc_htmx is a shortcut that we wrote to apply a function to elements matching a selector, including the element that triggered the event. Here’s the code for reference:\nexport function proc_htmx(sel, func) {\n htmx.onLoad(elt => {\n const elements = htmx.findAll(elt, sel);\n if (elt.matches(sel)) elements.unshift(elt)\n elements.forEach(func);\n });\n}\nThe AI Pictionary example uses a larger chunk of custom JavaScript to handle the drawing canvas. It’s a good example of the type of application where running code on the client side makes the most sense, but still shows how you can integrate it with FastHTML on the server side to add functionality (like the AI responses) easily.\nAdding styling with custom CSS and libraries such as tailwind is done the same way we add custom JavaScript. The doodle example uses Doodle.CSS to style the page in a quirky way.",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "FastHTML By Example"
+ ]
+ },
+ {
+ "objectID": "tutorials/by_example.html#deploying-your-app",
+ "href": "tutorials/by_example.html#deploying-your-app",
+ "title": "FastHTML By Example",
+ "section": "Deploying Your App",
+ "text": "Deploying Your App\nWe can deploy FastHTML almost anywhere you can deploy python apps. We’ve tested Railway, Replit, HuggingFace, and PythonAnywhere.\n\nRailway\n\nInstall the Railway CLI and sign up for an account.\nSet up a folder with our app as main.py\nIn the folder, run railway login.\nUse the fh_railway_deploy script to deploy our project:\n\nfh_railway_deploy MY_APP_NAME\nWhat the script does for us:\n\nDo we have an existing railway project?\n\nYes: Link the project folder to our existing Railway project.\nNo: Create a new Railway project.\n\nDeploy the project. We’ll see the logs as the service is built and run!\nFetches and displays the URL of our app.\nBy default, mounts a /app/data folder on the cloud to our app’s root folder. The app is run in /app by default, so from our app anything we store in /data will persist across restarts.\n\nA final note about Railway: We can add secrets like API keys that can be accessed as environment variables from our apps via ‘Variables’. For example, for the image app (TODO link), we can add a REPLICATE_API_KEY variable, and then in main.py we can access it as os.environ['REPLICATE_API_KEY'].\n\n\nReplit\nFork this repl for a minimal example you can edit to your heart’s content. .replit has been edited to add the right run command (run = [\"uvicorn\", \"main:app\", \"--reload\"]) and to set up the ports correctly. FastHTML was installed with poetry add python-fasthtml, you can add additional packages as needed in the same way. Running the app in Replit will show you a webview, but you may need to open in a new tab for all features (such as cookies) to work. When you’re ready, you can deploy your app by clicking the ‘Deploy’ button. You pay for usage - for an app that is mostly idle the cost is usually a few cents per month.\nYou can store secrets like API keys via the ‘Secrets’ tab in the Replit project settings.\n\n\nHuggingFace\nFollow the instructions in this repository to deploy to HuggingFace spaces.",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "FastHTML By Example"
+ ]
+ },
+ {
+ "objectID": "tutorials/by_example.html#where-next",
+ "href": "tutorials/by_example.html#where-next",
+ "title": "FastHTML By Example",
+ "section": "Where Next?",
+ "text": "Where Next?\nWe’ve covered a lot of ground here! Hopefully this has given you plenty to work with in building your own FastHTML apps. If you have any questions, feel free to ask in the #fasthtml Discord channel (in the fastai Discord community). You can look through the other examples in the fasthtml-example repository for more ideas, and keep an eye on Jeremy’s YouTube channel where we’ll be releasing a number of “dev chats” related to FastHTML in the near future.",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "FastHTML By Example"
+ ]
+ },
+ {
+ "objectID": "tutorials/e2e.html",
+ "href": "tutorials/e2e.html",
+ "title": "JS App Walkthrough",
+ "section": "",
+ "text": "You’ll need the following software to complete the tutorial, read on for specific installation instructions:\n\nPython\nA Python package manager such as pip (which normally comes with Python) or uv\nFastHTML\nWeb browser\nRailway.app account\n\nIf you haven’t worked with Python before, we recommend getting started with Miniconda.\nNote that you will only need to follow the steps in the installation section once per environment. If you create a new repo, you won’t need to redo these.\n\n\nFor Mac, Windows and Linux, enter:\npip install python-fasthtml",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "JS App Walkthrough"
+ ]
+ },
+ {
+ "objectID": "tutorials/e2e.html#installation",
+ "href": "tutorials/e2e.html#installation",
+ "title": "JS App Walkthrough",
+ "section": "",
+ "text": "You’ll need the following software to complete the tutorial, read on for specific installation instructions:\n\nPython\nA Python package manager such as pip (which normally comes with Python) or uv\nFastHTML\nWeb browser\nRailway.app account\n\nIf you haven’t worked with Python before, we recommend getting started with Miniconda.\nNote that you will only need to follow the steps in the installation section once per environment. If you create a new repo, you won’t need to redo these.\n\n\nFor Mac, Windows and Linux, enter:\npip install python-fasthtml",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "JS App Walkthrough"
+ ]
+ },
+ {
+ "objectID": "tutorials/e2e.html#first-steps",
+ "href": "tutorials/e2e.html#first-steps",
+ "title": "JS App Walkthrough",
+ "section": "First steps",
+ "text": "First steps\nBy the end of this section you’ll have your own FastHTML website with tests deployed to railway.app.\n\nCreate a hello world\nCreate a new folder to organize all the files for your project. Inside this folder, create a file called main.py and add the following code to it:\n\n\nmain.py\n\nfrom fasthtml.common import *\n\napp = FastHTML()\nrt = app.route\n\n@rt('/')\ndef get():\n return 'Hello, world!'\n\nserve()\n\nFinally, run python main.py in your terminal and open your browser to the ‘Link’ that appears.\n\n\nQuickDraw: A FastHTML Adventure 🎨✨\nThe end result of this tutorial will be QuickDraw, a real-time collaborative drawing app using FastHTML. Here is what the final site will look like:\n\n\n\nQuickDraw\n\n\n\nDrawing Rooms\nDrawing rooms are the core concept of our application. Each room represents a separate drawing space where a user can let their inner Picasso shine. Here’s a detailed breakdown:\n\nRoom Creation and Storage\n\n\n\nmain.py\n\ndb = database('data/drawapp.db')\nrooms = db.t.rooms\nif rooms not in db.t:\n rooms.create(id=int, name=str, created_at=str, pk='id')\nRoom = rooms.dataclass()\n\n@patch\ndef __ft__(self:Room):\n return Li(A(self.name, href=f\"/rooms/{self.id}\"))\n\nOr you can use our fast_app function to create a FastHTML app with a SQLite database and dataclass in one line:\n\n\nmain.py\n\ndef render(room):\n return Li(A(room.name, href=f\"/rooms/{room.id}\"))\n\napp,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, pk='id')\n\nWe are specifying a render function to convert our dataclass into HTML, which is the same as extending the __ft__ method from the patch decorator we used before. We will use this method for the rest of the tutorial since it is a lot cleaner and easier to read.\n\nWe’re using a SQLite database (via FastLite) to store our rooms.\nEach room has an id (integer), a name (string), and a created_at timestamp (string).\nThe Room dataclass is automatically generated based on this structure.\n\n\nCreating a room\n\n\n\nmain.py\n\n@rt(\"/\")\ndef get():\n # The 'Input' id defaults to the same as the name, so you can omit it if you wish\n create_room = Form(Input(id=\"name\", name=\"name\", placeholder=\"New Room Name\"),\n Button(\"Create Room\"),\n hx_post=\"/rooms\", hx_target=\"#rooms-list\", hx_swap=\"afterbegin\")\n rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list')\n return Titled(\"DrawCollab\", \n H1(\"DrawCollab\"),\n create_room, rooms_list)\n\n@rt(\"/rooms\")\nasync def post(room:Room):\n room.created_at = datetime.now().isoformat()\n return rooms.insert(room)\n\n\nWhen a user submits the “Create Room” form, this route is called.\nIt creates a new Room object, sets the creation time, and inserts it into the database.\nIt returns an HTML list item with a link to the new room, which is dynamically added to the room list on the homepage thanks to HTMX.\n\n\nLet’s give our rooms shape\n\n\n\nmain.py\n\n@rt(\"/rooms/{id}\")\nasync def get(id:int):\n room = rooms[id]\n return Titled(f\"Room: {room.name}\", H1(f\"Welcome to {room.name}\"), A(Button(\"Leave Room\"), href=\"/\"))\n\n\nThis route renders the interface for a specific room.\nIt fetches the room from the database and renders a title, heading, and paragraph.\n\nHere is the full code so far:\n\n\nmain.py\n\nfrom fasthtml.common import *\nfrom datetime import datetime\n\ndef render(room):\n return Li(A(room.name, href=f\"/rooms/{room.id}\"))\n\napp,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, pk='id')\n\n@rt(\"/\")\ndef get():\n create_room = Form(Input(id=\"name\", name=\"name\", placeholder=\"New Room Name\"),\n Button(\"Create Room\"),\n hx_post=\"/rooms\", hx_target=\"#rooms-list\", hx_swap=\"afterbegin\")\n rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list')\n return Titled(\"DrawCollab\", create_room, rooms_list)\n\n@rt(\"/rooms\")\nasync def post(room:Room):\n room.created_at = datetime.now().isoformat()\n return rooms.insert(room)\n\n@rt(\"/rooms/{id}\")\nasync def get(id:int):\n room = rooms[id]\n return Titled(f\"Room: {room.name}\", H1(f\"Welcome to {room.name}\"), A(Button(\"Leave Room\"), href=\"/\"))\n\nserve()\n\nNow run python main.py in your terminal and open your browser to the ‘Link’ that appears. You should see a page with a form to create a new room and a list of existing rooms.\n\n\nThe Canvas - Let’s Get Drawing! 🖌️\nTime to add the actual drawing functionality. We’ll use Fabric.js for this:\n\n\nmain.py\n\n# ... (keep the previous imports and database setup)\n\n@rt(\"/rooms/{id}\")\nasync def get(id:int):\n room = rooms[id]\n canvas = Canvas(id=\"canvas\", width=\"800\", height=\"600\")\n color_picker = Input(type=\"color\", id=\"color-picker\", value=\"#3CDD8C\")\n brush_size = Input(type=\"range\", id=\"brush-size\", min=\"1\", max=\"50\", value=\"10\")\n \n js = \"\"\"\n var canvas = new fabric.Canvas('canvas');\n canvas.isDrawingMode = true;\n canvas.freeDrawingBrush.color = '#3CDD8C';\n canvas.freeDrawingBrush.width = 10;\n \n document.getElementById('color-picker').onchange = function() {\n canvas.freeDrawingBrush.color = this.value;\n };\n \n document.getElementById('brush-size').oninput = function() {\n canvas.freeDrawingBrush.width = parseInt(this.value, 10);\n };\n \"\"\"\n \n return Titled(f\"Room: {room.name}\",\n A(Button(\"Leave Room\"), href=\"/\"),\n canvas,\n Div(color_picker, brush_size),\n Script(src=\"https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js\"),\n Script(js))\n\n# ... (keep the serve() part)\n\nNow we’ve got a drawing canvas! FastHTML makes it easy to include external libraries and add custom JavaScript.\n\n\nSaving and Loading Canvases 💾\nNow that we have a working drawing canvas, let’s add the ability to save and load drawings. We’ll modify our database schema to include a canvas_data field, and add new routes for saving and loading canvas data. Here’s how we’ll update our code:\n\nModify the database schema:\n\n\n\nmain.py\n\napp,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, canvas_data=str, pk='id')\n\n\nAdd a save button that grabs the canvas’ state and sends it to the server:\n\n\n\nmain.py\n\n@rt(\"/rooms/{id}\")\nasync def get(id:int):\n room = rooms[id]\n canvas = Canvas(id=\"canvas\", width=\"800\", height=\"600\")\n color_picker = Input(type=\"color\", id=\"color-picker\", value=\"#3CDD8C\")\n brush_size = Input(type=\"range\", id=\"brush-size\", min=\"1\", max=\"50\", value=\"10\")\n save_button = Button(\"Save Canvas\", id=\"save-canvas\", hx_post=f\"/rooms/{id}/save\", hx_vals=\"js:{canvas_data: JSON.stringify(canvas.toJSON())}\")\n # ... (rest of the function remains the same)\n\n\nAdd routes for saving and loading canvas data:\n\n\n\nmain.py\n\n@rt(\"/rooms/{id}/save\")\nasync def post(id:int, canvas_data:str):\n rooms.update({'canvas_data': canvas_data}, id)\n return \"Canvas saved successfully\"\n\n@rt(\"/rooms/{id}/load\")\nasync def get(id:int):\n room = rooms[id]\n return room.canvas_data if room.canvas_data else \"{}\"\n\n\nUpdate the JavaScript to load existing canvas data:\n\n\n\nmain.py\n\njs = f\"\"\"\n var canvas = new fabric.Canvas('canvas');\n canvas.isDrawingMode = true;\n canvas.freeDrawingBrush.color = '#3CDD8C';\n canvas.freeDrawingBrush.width = 10;\n // Load existing canvas data\n fetch(`/rooms/{id}/load`)\n .then(response => response.json())\n .then(data => {{\n if (data && Object.keys(data).length > 0) {{\n canvas.loadFromJSON(data, canvas.renderAll.bind(canvas));\n }}\n }});\n \n // ... (rest of the JavaScript remains the same)\n\"\"\"\n\nWith these changes, users can now save their drawings and load them when they return to the room. The canvas data is stored as a JSON string in the database, allowing for easy serialization and deserialization. Try it out! Create a new room, make a drawing, save it, and then reload the page. You should see your drawing reappear, ready for further editing.\nHere is the completed code:\n\n\nmain.py\n\nfrom fasthtml.common import *\nfrom datetime import datetime\n\ndef render(room):\n return Li(A(room.name, href=f\"/rooms/{room.id}\"))\n\napp,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, canvas_data=str, pk='id')\n\n@rt(\"/\")\ndef get():\n create_room = Form(Input(id=\"name\", name=\"name\", placeholder=\"New Room Name\"),\n Button(\"Create Room\"),\n hx_post=\"/rooms\", hx_target=\"#rooms-list\", hx_swap=\"afterbegin\")\n rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list')\n return Titled(\"QuickDraw\", \n create_room, rooms_list)\n\n@rt(\"/rooms\")\nasync def post(room:Room):\n room.created_at = datetime.now().isoformat()\n return rooms.insert(room)\n\n@rt(\"/rooms/{id}\")\nasync def get(id:int):\n room = rooms[id]\n canvas = Canvas(id=\"canvas\", width=\"800\", height=\"600\")\n color_picker = Input(type=\"color\", id=\"color-picker\", value=\"#000000\")\n brush_size = Input(type=\"range\", id=\"brush-size\", min=\"1\", max=\"50\", value=\"10\")\n save_button = Button(\"Save Canvas\", id=\"save-canvas\", hx_post=f\"/rooms/{id}/save\", hx_vals=\"js:{canvas_data: JSON.stringify(canvas.toJSON())}\")\n\n js = f\"\"\"\n var canvas = new fabric.Canvas('canvas');\n canvas.isDrawingMode = true;\n canvas.freeDrawingBrush.color = '#000000';\n canvas.freeDrawingBrush.width = 10;\n\n // Load existing canvas data\n fetch(`/rooms/{id}/load`)\n .then(response => response.json())\n .then(data => {{\n if (data && Object.keys(data).length > 0) {{\n canvas.loadFromJSON(data, canvas.renderAll.bind(canvas));\n }}\n }});\n \n document.getElementById('color-picker').onchange = function() {{\n canvas.freeDrawingBrush.color = this.value;\n }};\n \n document.getElementById('brush-size').oninput = function() {{\n canvas.freeDrawingBrush.width = parseInt(this.value, 10);\n }};\n \"\"\"\n \n return Titled(f\"Room: {room.name}\",\n A(Button(\"Leave Room\"), href=\"/\"),\n canvas,\n Div(color_picker, brush_size, save_button),\n Script(src=\"https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js\"),\n Script(js))\n\n@rt(\"/rooms/{id}/save\")\nasync def post(id:int, canvas_data:str):\n rooms.update({'canvas_data': canvas_data}, id)\n return \"Canvas saved successfully\"\n\n@rt(\"/rooms/{id}/load\")\nasync def get(id:int):\n room = rooms[id]\n return room.canvas_data if room.canvas_data else \"{}\"\n\nserve()\n\n\n\n\nDeploying to Railway\nYou can deploy your website to a number of hosting providers, for this tutorial we’ll be using Railway. To get started, make sure you create an account and install the Railway CLI. Once installed, make sure to run railway login to log in to your account.\nTo make deploying your website as easy as possible, FastHTMl comes with a built in CLI tool that will handle most of the deployment process for you. To deploy your website, run the following command in your terminal in the root directory of your project:\nfh_railway_deploy quickdraw\n\n\n\n\n\n\nNote\n\n\n\nYour app must be located in a main.py file for this to work.\n\n\n\n\nConclusion: You’re a FastHTML Artist Now! 🎨🚀\nCongratulations! You’ve just built a sleek, interactive web application using FastHTML. Let’s recap what we’ve learned:\n\nFastHTML allows you to create dynamic web apps with minimal code.\nWe used FastHTML’s routing system to handle different pages and actions.\nWe integrated with a SQLite database to store room information and canvas data.\nWe utilized Fabric.js to create an interactive drawing canvas.\nWe implemented features like color picking, brush size adjustment, and canvas saving.\nWe used HTMX for seamless, partial page updates without full reloads.\nWe learned how to deploy our FastHTML application to Railway for easy hosting.\n\nYou’ve taken your first steps into the world of FastHTML development. From here, the possibilities are endless! You could enhance the drawing app further by adding features like:\n\nImplementing different drawing tools (e.g., shapes, text)\nAdding user authentication\nCreating a gallery of saved drawings\nImplementing real-time collaborative drawing using WebSockets\n\nWhatever you choose to build next, FastHTML has got your back. Now go forth and create something awesome! Happy coding! 🖼️🚀",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "JS App Walkthrough"
+ ]
+ },
+ {
+ "objectID": "tutorials/index.html",
+ "href": "tutorials/index.html",
+ "title": "Tutorials",
+ "section": "",
+ "text": "Click through to any of these tutorials to get started with FastHTML’s features.\n\n\n\n\n\n\n\n\n\nTitle\n\n\nDescription\n\n\n\n\n\n\nFastHTML By Example\n\n\nAn introduction to FastHTML from the ground up, with four complete examples\n\n\n\n\nWeb Devs Quickstart\n\n\nA fast introduction to FastHTML for experienced web developers.\n\n\n\n\nJS App Walkthrough\n\n\nHow to build a website with custom JavaScript in FastHTML step-by-step\n\n\n\n\nUsing Jupyter to write FastHTML\n\n\nWriting FastHTML applications in Jupyter notebooks requires a slightly different process than normal Python applications.\n\n\n\n\n\nNo matching items",
+ "crumbs": [
+ "Home",
+ "Tutorials"
+ ]
+ },
+ {
+ "objectID": "tutorials/quickstart_for_web_devs.html",
+ "href": "tutorials/quickstart_for_web_devs.html",
+ "title": "Web Devs Quickstart",
+ "section": "",
+ "text": "pip install python-fasthtml",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "Web Devs Quickstart"
+ ]
+ },
+ {
+ "objectID": "tutorials/quickstart_for_web_devs.html#installation",
+ "href": "tutorials/quickstart_for_web_devs.html#installation",
+ "title": "Web Devs Quickstart",
+ "section": "",
+ "text": "pip install python-fasthtml",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "Web Devs Quickstart"
+ ]
+ },
+ {
+ "objectID": "tutorials/quickstart_for_web_devs.html#a-minimal-application",
+ "href": "tutorials/quickstart_for_web_devs.html#a-minimal-application",
+ "title": "Web Devs Quickstart",
+ "section": "A Minimal Application",
+ "text": "A Minimal Application\nA minimal FastHTML application looks something like this:\n\n\nmain.py\n\n1from fasthtml.common import *\n\n2app, rt = fast_app()\n\n3@rt(\"/\")\n4def get():\n5 return Titled(\"FastHTML\", P(\"Let's do this!\"))\n\n6serve()\n\n\n1\n\nWe import what we need for rapid development! A carefully-curated set of FastHTML functions and other Python objects is brought into our global namespace for convenience.\n\n2\n\nWe instantiate a FastHTML app with the fast_app() utility function. This provides a number of really useful defaults that we’ll take advantage of later in the tutorial.\n\n3\n\nWe use the rt() decorator to tell FastHTML what to return when a user visits / in their browser.\n\n4\n\nWe connect this route to HTTP GET requests by defining a view function called get().\n\n5\n\nA tree of Python function calls that return all the HTML required to write a properly formed web page. You’ll soon see the power of this approach.\n\n6\n\nThe serve() utility configures and runs FastHTML using a library called uvicorn.\n\n\nRun the code:\npython main.py\nThe terminal will look like this:\nINFO: Uvicorn running on http://0.0.0.0:5001 (Press CTRL+C to quit)\nINFO: Started reloader process [58058] using WatchFiles\nINFO: Started server process [58060]\nINFO: Waiting for application startup.\nINFO: Application startup complete.\nConfirm FastHTML is running by opening your web browser to 127.0.0.1:5001. You should see something like the image below:\n\n\n\n\n\n\n\nNote\n\n\n\nWhile some linters and developers will complain about the wildcard import, it is by design here and perfectly safe. FastHTML is very deliberate about the objects it exports in fasthtml.common. If it bothers you, you can import the objects you need individually, though it will make the code more verbose and less readable.\nIf you want to learn more about how FastHTML handles imports, we cover that here.",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "Web Devs Quickstart"
+ ]
+ },
+ {
+ "objectID": "tutorials/quickstart_for_web_devs.html#a-minimal-charting-application",
+ "href": "tutorials/quickstart_for_web_devs.html#a-minimal-charting-application",
+ "title": "Web Devs Quickstart",
+ "section": "A Minimal Charting Application",
+ "text": "A Minimal Charting Application\nThe Script function allows you to include JavaScript. You can use Python to generate parts of your JS or JSON like this:\nimport json\nfrom fasthtml.common import * \n\napp, rt = fast_app(hdrs=(Script(src=\"https://cdn.plot.ly/plotly-2.32.0.min.js\"),))\n\ndata = json.dumps({\n \"data\": [{\"x\": [1, 2, 3, 4],\"type\": \"scatter\"},\n {\"x\": [1, 2, 3, 4],\"y\": [16, 5, 11, 9],\"type\": \"scatter\"}],\n \"title\": \"Plotly chart in FastHTML \",\n \"description\": \"This is a demo dashboard\",\n \"type\": \"scatter\"\n})\n\n\n@rt(\"/\")\ndef get():\n return Titled(\"Chart Demo\", Div(id=\"myDiv\"),\n Script(f\"var data = {data}; Plotly.newPlot('myDiv', data);\"))\n\nserve()",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "Web Devs Quickstart"
+ ]
+ },
+ {
+ "objectID": "tutorials/quickstart_for_web_devs.html#debug-mode",
+ "href": "tutorials/quickstart_for_web_devs.html#debug-mode",
+ "title": "Web Devs Quickstart",
+ "section": "Debug Mode",
+ "text": "Debug Mode\nWhen we can’t figure out a bug in FastHTML, we can run it in DEBUG mode. When an error is thrown, the error screen is displayed in the browser. This error setting should never be used in a deployed app.\nfrom fasthtml.common import *\n\n1app, rt = fast_app(debug=True)\n\n@rt(\"/\")\ndef get():\n2 1/0\n return Titled(\"FastHTML Error!\", P(\"Let's error!\"))\n\nserve()\n\n1\n\ndebug=True sets debug mode on.\n\n2\n\nPython throws an error when it tries to divide an integer by zero.",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "Web Devs Quickstart"
+ ]
+ },
+ {
+ "objectID": "tutorials/quickstart_for_web_devs.html#routing",
+ "href": "tutorials/quickstart_for_web_devs.html#routing",
+ "title": "Web Devs Quickstart",
+ "section": "Routing",
+ "text": "Routing\nFastHTML builds upon FastAPI’s friendly decorator pattern for specifying URLs, with extra features:\n\n\nmain.py\n\nfrom fasthtml.common import * \n\napp, rt = fast_app()\n\n1@rt(\"/\")\ndef get():\n return Titled(\"FastHTML\", P(\"Let's do this!\"))\n\n2@rt(\"/hello\")\ndef get():\n return Titled(\"Hello, world!\")\n\nserve()\n\n\n1\n\nThe “/” URL on line 5 is the home of a project. This would be accessed at 127.0.0.1:5001.\n\n2\n\n“/hello” URL on line 9 will be found by the project if the user visits 127.0.0.1:5001/hello.\n\n\n\n\n\n\n\n\nTip\n\n\n\nIt looks like get() is being defined twice, but that’s not the case. Each function decorated with rt is totally separate, and is injected into the router. We’re not calling them in the module’s namespace (locals()). Rather, we’re loading them into the routing mechanism using the rt decorator.\n\n\nYou can do more! Read on to learn what we can do to make parts of the URL dynamic.",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "Web Devs Quickstart"
+ ]
+ },
+ {
+ "objectID": "tutorials/quickstart_for_web_devs.html#variables-in-urls",
+ "href": "tutorials/quickstart_for_web_devs.html#variables-in-urls",
+ "title": "Web Devs Quickstart",
+ "section": "Variables in URLs",
+ "text": "Variables in URLs\nYou can add variable sections to a URL by marking them with {variable_name}. Your function then receives the {variable_name} as a keyword argument, but only if it is the correct type. Here’s an example:\n\n\nmain.py\n\nfrom fasthtml.common import * \n\napp, rt = fast_app()\n\n1@rt(\"/{name}/{age}\")\n2def get(name: str, age: int):\n3 return Titled(f\"Hello {name.title()}, age {age}\")\n\nserve()\n\n\n1\n\nWe specify two variable names, name and age.\n\n2\n\nWe define two function arguments named identically to the variables. You will note that we specify the Python types to be passed.\n\n3\n\nWe use these functions in our project.\n\n\nTry it out by going to this address: 127.0.0.1:5001/uma/5. You should get a page that says,\n\n“Hello Uma, age 5”.\n\n\nWhat happens if we enter incorrect data?\nThe 127.0.0.1:5001/uma/5 URL works because 5 is an integer. If we enter something that is not, such as 127.0.0.1:5001/uma/five, then FastHTML will return an error instead of a web page.\n\n\n\n\n\n\nFastHTML URL routing supports more complex types\n\n\n\nThe two examples we provide here use Python’s built-in str and int types, but you can use your own types, including more complex ones such as those defined by libraries like attrs, pydantic, and even sqlmodel.",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "Web Devs Quickstart"
+ ]
+ },
+ {
+ "objectID": "tutorials/quickstart_for_web_devs.html#http-methods",
+ "href": "tutorials/quickstart_for_web_devs.html#http-methods",
+ "title": "Web Devs Quickstart",
+ "section": "HTTP Methods",
+ "text": "HTTP Methods\nFastHTML matches function names to HTTP methods. So far the URL routes we’ve defined have been for HTTP GET methods, the most common method for web pages.\nForm submissions often are sent as HTTP POST. When dealing with more dynamic web page designs, also known as Single Page Apps (SPA for short), the need can arise for other methods such as HTTP PUT and HTTP DELETE. The way FastHTML handles this is by changing the function name.\n\n\nmain.py\n\nfrom fasthtml.common import * \n\napp, rt = fast_app()\n\n@rt(\"/\") \n1def get():\n return Titled(\"HTTP GET\", P(\"Handle GET\"))\n\n@rt(\"/\") \n2def post():\n return Titled(\"HTTP POST\", P(\"Handle POST\"))\n\nserve()\n\n\n1\n\nOn line 6 because the get() function name is used, this will handle HTTP GETs going to the / URI.\n\n2\n\nOn line 10 because the post() function name is used, this will handle HTTP POSTs going to the / URI.",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "Web Devs Quickstart"
+ ]
+ },
+ {
+ "objectID": "tutorials/quickstart_for_web_devs.html#css-files-and-inline-styles",
+ "href": "tutorials/quickstart_for_web_devs.html#css-files-and-inline-styles",
+ "title": "Web Devs Quickstart",
+ "section": "CSS Files and Inline Styles",
+ "text": "CSS Files and Inline Styles\nHere we modify default headers to demonstrate how to use the Sakura CSS microframework instead of FastHTML’s default of Pico CSS.\n\n\nmain.py\n\nfrom fasthtml.common import * \n\napp, rt = fast_app(\n1 pico=False,\n hdrs=(\n Link(rel='stylesheet', href='assets/normalize.min.css', type='text/css'),\n2 Link(rel='stylesheet', href='assets/sakura.css', type='text/css'),\n3 Style(\"p {color: red;}\")\n))\n\n@app.get(\"/\")\ndef home():\n return Titled(\"FastHTML\",\n P(\"Let's do this!\"),\n )\n\nserve()\n\n\n1\n\nBy setting pico to False, FastHTML will not include pico.min.css.\n\n2\n\nThis will generate an HTML <link> tag for sourcing the css for Sakura.\n\n3\n\nIf you want an inline styles, the Style() function will put the result into the HTML.",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "Web Devs Quickstart"
+ ]
+ },
+ {
+ "objectID": "tutorials/quickstart_for_web_devs.html#other-static-media-file-locations",
+ "href": "tutorials/quickstart_for_web_devs.html#other-static-media-file-locations",
+ "title": "Web Devs Quickstart",
+ "section": "Other Static Media File Locations",
+ "text": "Other Static Media File Locations\nAs you saw, Script and Link are specific to the most common static media use cases in web apps: including JavaScript, CSS, and images. But it also works with videos and other static media files. The default behavior is to look for these files in the root directory - typically we don’t do anything special to include them. We can change the default directory that is looked in for files by adding the static_path parameter to the fast_app function.\napp, rt = fast_app(static_path='public')\nFastHTML also allows us to define a route that uses FileResponse to serve the file at a specified path. This is useful for serving images, videos, and other media files from a different directory without having to change the paths of many files. So if we move the directory containing the media files, we only need to change the path in one place. In the example below, we call images from a directory called public.\n@rt(\"/{fname:path}.{ext:static}\")\nasync def get(fname:str, ext:str): \n return FileResponse(f'public/{fname}.{ext}')",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "Web Devs Quickstart"
+ ]
+ },
+ {
+ "objectID": "tutorials/quickstart_for_web_devs.html#rendering-markdown",
+ "href": "tutorials/quickstart_for_web_devs.html#rendering-markdown",
+ "title": "Web Devs Quickstart",
+ "section": "Rendering Markdown",
+ "text": "Rendering Markdown\nfrom fasthtml.common import *\n\nhdrs = (MarkdownJS(), HighlightJS(langs=['python', 'javascript', 'html', 'css']), )\n\napp, rt = fast_app(hdrs=hdrs)\n\ncontent = \"\"\"\nHere are some _markdown_ elements.\n\n- This is a list item\n- This is another list item\n- And this is a third list item\n\n**Fenced code blocks work here.**\n\"\"\"\n\n@rt('/')\ndef get(req):\n return Titled(\"Markdown rendering example\", Div(content,cls=\"marked\"))\n\nserve()",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "Web Devs Quickstart"
+ ]
+ },
+ {
+ "objectID": "tutorials/quickstart_for_web_devs.html#code-highlighting",
+ "href": "tutorials/quickstart_for_web_devs.html#code-highlighting",
+ "title": "Web Devs Quickstart",
+ "section": "Code highlighting",
+ "text": "Code highlighting\nHere’s how to highlight code without any markdown configuration.\nfrom fasthtml.common import *\n\n# Add the HighlightJS built-in header\nhdrs = (HighlightJS(langs=['python', 'javascript', 'html', 'css']),)\n\napp, rt = fast_app(hdrs=hdrs)\n\ncode_example = \"\"\"\nimport datetime\nimport time\n\nfor i in range(10):\n print(f\"{datetime.datetime.now()}\")\n time.sleep(1)\n\"\"\"\n\n@rt('/')\ndef get(req):\n return Titled(\"Markdown rendering example\",\n Div(\n # The code example needs to be surrounded by\n # Pre & Code elements\n Pre(Code(code_example))\n ))\n\nserve()",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "Web Devs Quickstart"
+ ]
+ },
+ {
+ "objectID": "tutorials/quickstart_for_web_devs.html#defining-new-ft-components",
+ "href": "tutorials/quickstart_for_web_devs.html#defining-new-ft-components",
+ "title": "Web Devs Quickstart",
+ "section": "Defining new ft components",
+ "text": "Defining new ft components\nWe can build our own ft components and combine them with other components. The simplest method is defining them as a function.\n\nfrom fasthtml.common import *\n\n\ndef hero(title, statement):\n return Div(H1(title),P(statement), cls=\"hero\")\n\n# usage example\nMain(\n hero(\"Hello World\", \"This is a hero statement\")\n)\n\n<main> <div class=\"hero\">\n <h1>Hello World</h1>\n <p>This is a hero statement</p>\n </div>\n</main>\n\n\n\nPass through components\nFor when we need to define a new component that allows zero-to-many components to be nested within them, we lean on Python’s *args and **kwargs mechanism. Useful for creating page layout controls.\n\ndef layout(*args, **kwargs):\n \"\"\"Dashboard layout for all our dashboard views\"\"\"\n return Main(\n H1(\"Dashboard\"),\n Div(*args, **kwargs),\n cls=\"dashboard\",\n )\n\n# usage example\nlayout(\n Ul(*[Li(o) for o in range(3)]),\n P(\"Some content\", cls=\"description\"),\n)\n\n<main class=\"dashboard\"> <h1>Dashboard</h1>\n <div>\n <ul>\n <li>0</li>\n <li>1</li>\n <li>2</li>\n </ul>\n <p class=\"description\">Some content</p>\n </div>\n</main>\n\n\n\n\nDataclasses as ft components\nWhile functions are easy to read, for more complex components some might find it easier to use a dataclass.\n\nfrom dataclasses import dataclass\n\n@dataclass\nclass Hero:\n title: str\n statement: str\n \n def __ft__(self):\n \"\"\" The __ft__ method renders the dataclass at runtime.\"\"\"\n return Div(H1(self.title),P(self.statement), cls=\"hero\")\n \n# usage example\nMain(\n Hero(\"Hello World\", \"This is a hero statement\")\n)\n\n<main> <div class=\"hero\">\n <h1>Hello World</h1>\n <p>This is a hero statement</p>\n </div>\n</main>",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "Web Devs Quickstart"
+ ]
+ },
+ {
+ "objectID": "tutorials/quickstart_for_web_devs.html#testing-views-in-notebooks",
+ "href": "tutorials/quickstart_for_web_devs.html#testing-views-in-notebooks",
+ "title": "Web Devs Quickstart",
+ "section": "Testing views in notebooks",
+ "text": "Testing views in notebooks\nBecause of the ASGI event loop it is currently impossible to run FastHTML inside a notebook. However, we can still test the output of our views. To do this, we leverage Starlette, an ASGI toolkit that FastHTML uses.\n\n# First we instantiate our app, in this case we remove the\n# default headers to reduce the size of the output.\napp, rt = fast_app(default_hdrs=False)\n\n# Setting up the Starlette test client\nfrom starlette.testclient import TestClient\nclient = TestClient(app)\n\n# Usage example\n@rt(\"/\")\ndef get():\n return Titled(\"FastHTML is awesome\", \n P(\"The fastest way to create web apps in Python\"))\n\nprint(client.get(\"/\").text)\n\n <!doctype html>\n <html>\n <head>\n<title>FastHTML is awesome</title> </head>\n <body>\n<main class=\"container\"> <h1>FastHTML is awesome</h1>\n <p>The fastest way to create web apps in Python</p>\n</main> </body>\n </html>",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "Web Devs Quickstart"
+ ]
+ },
+ {
+ "objectID": "tutorials/quickstart_for_web_devs.html#forms",
+ "href": "tutorials/quickstart_for_web_devs.html#forms",
+ "title": "Web Devs Quickstart",
+ "section": "Forms",
+ "text": "Forms\nTo validate data coming from users, first define a dataclass representing the data you want to check. Here’s an example representing a signup form.\n\nfrom dataclasses import dataclass\n\n@dataclass\nclass Profile: email:str; phone:str; age:int\n\nCreate an FT component representing an empty version of that form. Don’t pass in any value to fill the form, that gets handled later.\n\nprofile_form = Form(method=\"post\", action=\"/profile\")(\n Fieldset(\n Label('Email', Input(name=\"email\")),\n Label(\"Phone\", Input(name=\"phone\")),\n Label(\"Age\", Input(name=\"age\")),\n ),\n Button(\"Save\", type=\"submit\"),\n )\nprofile_form\n\n<form enctype=\"multipart/form-data\" method=\"post\" action=\"/profile\"><fieldset><label>Email <input name=\"email\">\n</label><label>Phone <input name=\"phone\">\n</label><label>Age <input name=\"age\">\n</label></fieldset><button type=\"submit\">Save</button></form>\n\n\nOnce the dataclass and form function are completed, we can add data to the form. To do that, instantiate the profile dataclass:\n\nprofile = Profile(email='john@example.com', phone='123456789', age=5)\nprofile\n\nProfile(email='john@example.com', phone='123456789', age=5)\n\n\nThen add that data to the profile_form using FastHTML’s fill_form class:\n\nfill_form(profile_form, profile)\n\n<form enctype=\"multipart/form-data\" method=\"post\" action=\"/profile\"><fieldset><label>Email <input name=\"email\" value=\"john@example.com\">\n</label><label>Phone <input name=\"phone\" value=\"123456789\">\n</label><label>Age <input name=\"age\" value=\"5\">\n</label></fieldset><button type=\"submit\">Save</button></form>\n\n\n\nForms with views\nThe usefulness of FastHTML forms becomes more apparent when they are combined with FastHTML views. We’ll show how this works by using the test client from above. First, let’s create a SQlite database:\n\ndb = Database(\"profiles.db\")\nprofiles = db.create(Profile, pk=\"email\")\n\nNow we insert a record into the database:\n\nprofiles.insert(profile)\n\nProfile(email='john@example.com', phone='123456789', age=5)\n\n\nAnd we can then demonstrate in the code that form is filled and displayed to the user.\n\n@rt(\"/profile/{email}\")\ndef profile(email:str):\n1 profile = profiles[email]\n2 filled_profile_form = fill_form(profile_form, profile)\n return Titled(f'Profile for {profile.email}', filled_profile_form)\n\nprint(client.get(f\"/profile/john@example.com\").text)\n\n\n1\n\nFetch the profile using the profile table’s email primary key\n\n2\n\nFill the form for display.\n\n\n\n\n <!doctype html>\n <html>\n <head>\n<title>Profile for john@example.com</title> </head>\n <body>\n<main class=\"container\"> <h1>Profile for john@example.com</h1>\n<form enctype=\"multipart/form-data\" method=\"post\" action=\"/profile\"><fieldset><label>Email <input name=\"email\" value=\"john@example.com\">\n</label><label>Phone <input name=\"phone\" value=\"123456789\">\n</label><label>Age <input name=\"age\" value=\"5\">\n</label></fieldset><button type=\"submit\">Save</button></form></main> </body>\n </html>\n\n\n\nAnd now let’s demonstrate making a change to the data.\n\n@rt(\"/profile\")\n1def post(profile: Profile):\n2 profiles.update(profile)\n3 return RedirectResponse(url=f\"/profile/{profile.email}\")\n\nnew_data = dict(email='john@example.com', phone='7654321', age=25)\n4print(client.post(\"/profile\", data=new_data).text)\n\n\n1\n\nWe use the Profile dataclass definition to set the type for the incoming profile content. This validates the field types for the incoming data\n\n2\n\nTaking our validated data, we updated the profiles table\n\n3\n\nWe redirect the user back to their profile view\n\n4\n\nThe display is of the profile form view showing the changes in data.\n\n\n\n\n <!doctype html>\n <html>\n <head>\n<title>Profile for john@example.com</title> </head>\n <body>\n<main class=\"container\"> <h1>Profile for john@example.com</h1>\n<form enctype=\"multipart/form-data\" method=\"post\" action=\"/profile\"><fieldset><label>Email <input name=\"email\" value=\"john@example.com\">\n</label><label>Phone <input name=\"phone\" value=\"7654321\">\n</label><label>Age <input name=\"age\" value=\"25\">\n</label></fieldset><button type=\"submit\">Save</button></form></main> </body>\n </html>",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "Web Devs Quickstart"
+ ]
+ },
+ {
+ "objectID": "tutorials/quickstart_for_web_devs.html#strings-and-conversion-order",
+ "href": "tutorials/quickstart_for_web_devs.html#strings-and-conversion-order",
+ "title": "Web Devs Quickstart",
+ "section": "Strings and conversion order",
+ "text": "Strings and conversion order\nThe general rules for rendering are: - __ft__ method will be called (for default components like P, H2, etc. or if you define your own components) - If you pass a string, it will be escaped - On other python objects, str() will be called\nAs a consequence, if you want to include plain HTML tags directly into e.g. a Div() they will get escaped by default (as a security measure to avoid code injections). This can be avoided by using NotStr(), a convenient way to reuse python code that returns already HTML. If you use pandas, you can use pandas.DataFrame.to_html() to get a nice table. To include the output a FastHTML, wrap it in NotStr(), like Div(NotStr(df.to_html())).\nAbove we saw how a dataclass behaves with the __ft__ method defined. On a plain dataclass, str() will be called (but not escaped).\n\nfrom dataclasses import dataclass\n\n@dataclass\nclass Hero:\n title: str\n statement: str\n \n# rendering the dataclass with the default method\nMain(\n Hero(\"<h1>Hello World</h1>\", \"This is a hero statement\")\n)\n\n<main>Hero(title='<h1>Hello World</h1>', statement='This is a hero statement')</main>\n\n\n\n# This will display the HTML as text on your page\nDiv(\"Let's include some HTML here: <div>Some HTML</div>\")\n\n<div>Let's include some HTML here: <div>Some HTML</div></div>\n\n\n\n# Keep the string untouched, will be rendered on the page\nDiv(NotStr(\"<div><h1>Some HTML</h1></div>\"))\n\n<div><div><h1>Some HTML</h1></div></div>",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "Web Devs Quickstart"
+ ]
+ },
+ {
+ "objectID": "tutorials/quickstart_for_web_devs.html#custom-exception-handlers",
+ "href": "tutorials/quickstart_for_web_devs.html#custom-exception-handlers",
+ "title": "Web Devs Quickstart",
+ "section": "Custom exception handlers",
+ "text": "Custom exception handlers\nFastHTML allows customization of exception handlers, but does so gracefully. What this means is by default it includes all the <html> tags needed to display attractive content. Try it out!\nfrom fasthtml.common import *\n\ndef not_found(req, exc): return Titled(\"404: I don't exist!\")\n\nexception_handlers = {404: not_found}\n\napp, rt = fast_app(exception_handlers=exception_handlers)\n\n@rt('/')\ndef get():\n return (Titled(\"Home page\", P(A(href=\"/oops\")(\"Click to generate 404 error\"))))\n\nserve()\nWe can also use lambda to make things more terse:\nfrom fasthtml.common import *\n\nexception_handlers={\n 404: lambda req, exc: Titled(\"404: I don't exist!\"),\n 418: lambda req, exc: Titled(\"418: I'm a teapot!\")\n}\n\napp, rt = fast_app(exception_handlers=exception_handlers)\n\n@rt('/')\ndef get():\n return (Titled(\"Home page\", P(A(href=\"/oops\")(\"Click to generate 404 error\"))))\n\nserve()",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "Web Devs Quickstart"
+ ]
+ },
+ {
+ "objectID": "tutorials/quickstart_for_web_devs.html#cookies",
+ "href": "tutorials/quickstart_for_web_devs.html#cookies",
+ "title": "Web Devs Quickstart",
+ "section": "Cookies",
+ "text": "Cookies\nWe can set cookies using the cookie() function. In our example, we’ll create a timestamp cookie.\n\nfrom datetime import datetime\nfrom IPython.display import HTML\n\n\n@rt(\"/settimestamp\")\ndef get(req):\n now = datetime.now()\n return P(f'Set to {now}'), cookie('now', datetime.now())\n\nHTML(client.get('/settimestamp').text)\n\n \n \n \nFastHTML page \n \n Set to 2024-09-26 15:33:48.141869\n \n \n\n\nNow let’s get it back using the same name for our parameter as the cookie name.\n\n@rt('/gettimestamp')\ndef get(now:parsed_date): return f'Cookie was set at time {now.time()}'\n\nclient.get('/gettimestamp').text\n\n'Cookie was set at time 15:33:48.141903'",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "Web Devs Quickstart"
+ ]
+ },
+ {
+ "objectID": "tutorials/quickstart_for_web_devs.html#sessions",
+ "href": "tutorials/quickstart_for_web_devs.html#sessions",
+ "title": "Web Devs Quickstart",
+ "section": "Sessions",
+ "text": "Sessions\nFor convenience and security, FastHTML has a mechanism for storing small amounts of data in the user’s browser. We can do this by adding a session argument to routes. FastHTML sessions are Python dictionaries, and we can leverage to our benefit. The example below shows how to concisely set and get sessions.\n\n@rt('/adder/{num}')\ndef get(session, num: int):\n session.setdefault('sum', 0)\n session['sum'] = session.get('sum') + num\n return Response(f'The sum is {session[\"sum\"]}.')",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "Web Devs Quickstart"
+ ]
+ },
+ {
+ "objectID": "tutorials/quickstart_for_web_devs.html#toasts-also-known-as-messages",
+ "href": "tutorials/quickstart_for_web_devs.html#toasts-also-known-as-messages",
+ "title": "Web Devs Quickstart",
+ "section": "Toasts (also known as Messages)",
+ "text": "Toasts (also known as Messages)\nToasts, sometimes called “Messages” are small notifications usually in colored boxes used to notify users that something has happened. Toasts can be of four types:\n\ninfo\nsuccess\nwarning\nerror\n\nExamples toasts might include:\n\n“Payment accepted”\n“Data submitted”\n“Request approved”\n\nToasts require the use of the setup_toasts() function plus every view needs these two features:\n\nThe session argument\nMust return FT components\n\n1setup_toasts(app)\n\n@rt('/toasting')\n2def get(session):\n # Normally one toast is enough, this allows us to see\n # different toast types in action.\n add_toast(session, f\"Toast is being cooked\", \"info\")\n add_toast(session, f\"Toast is ready\", \"success\")\n add_toast(session, f\"Toast is getting a bit crispy\", \"warning\")\n add_toast(session, f\"Toast is burning!\", \"error\")\n3 return Titled(\"I like toast\")\n\n1\n\nsetup_toasts is a helper function that adds toast dependencies. Usually this would be declared right after fast_app()\n\n2\n\nToasts require sessions\n\n3\n\nViews with Toasts must return FT components.",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "Web Devs Quickstart"
+ ]
+ },
+ {
+ "objectID": "tutorials/quickstart_for_web_devs.html#authentication-and-authorization",
+ "href": "tutorials/quickstart_for_web_devs.html#authentication-and-authorization",
+ "title": "Web Devs Quickstart",
+ "section": "Authentication and authorization",
+ "text": "Authentication and authorization\nIn FastHTML the tasks of authentication and authorization are handled with Beforeware. Beforeware are functions that run before the route handler is called. They are useful for global tasks like ensuring users are authenticated or have permissions to access a view.\nFirst, we write a function that accepts a request and session arguments:\n\n# Status code 303 is a redirect that can change POST to GET,\n# so it's appropriate for a login page.\nlogin_redir = RedirectResponse('/login', status_code=303)\n\ndef user_auth_before(req, sess):\n # The `auth` key in the request scope is automatically provided\n # to any handler which requests it, and can not be injected\n # by the user using query params, cookies, etc, so it should\n # be secure to use. \n auth = req.scope['auth'] = sess.get('auth', None)\n # If the session key is not there, it redirects to the login page.\n if not auth: return login_redir\n\nNow we pass our user_auth_before function as the first argument into a Beforeware class. We also pass a list of regular expressions to the skip argument, designed to allow users to still get to the home and login pages.\n\nbeforeware = Beforeware(\n user_auth_before,\n skip=[r'/favicon\\.ico', r'/static/.*', r'.*\\.css', r'.*\\.js', '/login', '/']\n)\n\napp, rt = fast_app(before=beforeware)",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "Web Devs Quickstart"
+ ]
+ },
+ {
+ "objectID": "tutorials/quickstart_for_web_devs.html#server-sent-events-sse",
+ "href": "tutorials/quickstart_for_web_devs.html#server-sent-events-sse",
+ "title": "Web Devs Quickstart",
+ "section": "Server-sent events (SSE)",
+ "text": "Server-sent events (SSE)\nWith server-sent events, it’s possible for a server to send new data to a web page at any time, by pushing messages to the web page. Unlike WebSockets, SSE can only go in one direction: server to client. SSE is also part of the HTTP specification unlike WebSockets which uses its own specification.\nFastHTML introduces several tools for working with SSE which are covered in the example below. While concise, there’s a lot going on in this function so we’ve annotated it quite a bit.\n\nimport random\nfrom asyncio import sleep\nfrom fasthtml.common import *\n\n1hdrs=(Script(src=\"https://unpkg.com/htmx-ext-sse@2.2.1/sse.js\"),)\napp,rt = fast_app(hdrs=hdrs)\n\n@rt\ndef index():\n return Titled(\"SSE Random Number Generator\",\n P(\"Generate pairs of random numbers, as the list grows scroll downwards.\"),\n2 Div(hx_ext=\"sse\",\n3 sse_connect=\"/number-stream\",\n4 hx_swap=\"beforeend show:bottom\",\n5 sse_swap=\"message\"))\n\n6shutdown_event = signal_shutdown()\n\n7async def number_generator():\n8 while not shutdown_event.is_set():\n data = Article(random.randint(1, 100))\n9 yield sse_message(data)\n await sleep(1)\n\n@rt(\"/number-stream\")\n10async def get(): return EventStream(number_generator())\n\n\n1\n\nImport the HTMX SSE extension\n\n2\n\nTell HTMX to load the SSE extension\n\n3\n\nLook at the /number-stream endpoint for SSE content\n\n4\n\nWhen new items come in from the SSE endpoint, add them at the end of the current content within the div. If they go beyond the screen, scroll downwards\n\n5\n\nSpecify the name of the event. FastHTML’s default event name is “message”. Only change if you have more than one call to SSE endpoints within a view\n\n6\n\nSet up the asyncio event loop\n\n7\n\nDon’t forget to make this an async function!\n\n8\n\nIterate through the asyncio event loop\n\n9\n\nWe yield the data. Data ideally should be comprised of FT components as that plugs nicely into HTMX in the browser\n\n10\n\nThe endpoint view needs to be an async function that returns a EventStream",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "Web Devs Quickstart"
+ ]
+ },
+ {
+ "objectID": "tutorials/quickstart_for_web_devs.html#websockets",
+ "href": "tutorials/quickstart_for_web_devs.html#websockets",
+ "title": "Web Devs Quickstart",
+ "section": "Websockets",
+ "text": "Websockets\nWith websockets we can have bi-directional communications between a browser and client. Websockets are useful for things like chat and certain types of games. While websockets can be used for single direction messages from the server (i.e. telling users that a process is finished), that task is arguably better suited for SSE.\nFastHTML provides useful tools for adding websockets to your pages.\n\nfrom fasthtml.common import *\nfrom asyncio import sleep\n\n1app, rt = fast_app(ws_hdr=True)\n\n2def mk_inp(): return Input(id='msg', autofocus=True)\n\n@rt('/')\nasync def get(request):\n cts = Div(\n Div(id='notifications'),\n3 Form(mk_inp(), id='form', ws_send=True),\n4 hx_ext='ws', ws_connect='/ws')\n return Titled('Websocket Test', cts)\n\n5async def on_connect(send):\n print('Connected!')\n6 await send(Div('Hello, you have connected', id=\"notifications\"))\n\n7async def on_disconnect(ws):\n print('Disconnected!')\n\n8@app.ws('/ws', conn=on_connect, disconn=on_disconnect)\n9async def ws(msg:str, send):\n10 await send(Div('Hello ' + msg, id=\"notifications\"))\n await sleep(2)\n11 return Div('Goodbye ' + msg, id=\"notifications\"), mk_inp()\n\n\n1\n\nTo use websockets in FastHTML, you must instantiate the app with the ws_hdr set to True\n\n2\n\nAs we want to use websockets to reset the form, we define the mk_input function that can be called from multiple locations\n\n3\n\nWe create the form and mark it with the ws_send attribute, which is documented here in the HTMX websocket specification. This tells HTMX to send a message to the nearest websocket based on the trigger for the form element, which for forms is pressing the enter key, an action considered to be a form submission\n\n4\n\nThis is where the HTMX extension is loaded (hx_ext='ws') and the nearest websocket is defined (ws_connect='/ws')\n\n5\n\nWhen a websocket first connects we can optionally have it call a function that accepts a send argument. The send argument will push a message to the browser.\n\n6\n\nHere we use the send function that was passed into the on_connect function to send a Div with an id of notifications that HTMX assigns to the element in the page that already has an id of notifications\n\n7\n\nWhen a websocket disconnects we can call a function which takes no arguments. Typically the role of this function is to notify the server to take an action. In this case, we print a simple message to the console\n\n8\n\nWe use the app.ws decorator to mark that /ws is the route for our websocket. We also pass in the two optional conn and disconn parameters to this decorator. As a fun experiment, remove the conn and disconn arguments and see what happens\n\n9\n\nDefine the ws function as async. This is necessary for ASGI to be able to serve websockets. The function accepts two arguments, a msg that is user input from the browser, and a send function for pushing data back to the browser\n\n10\n\nThe send function is used here to send HTML back to the page. As the HTML has an id of notifications, HTMX will overwrite what is already on the page with the same ID\n\n11\n\nThe websocket function can also be used to return a value. In this case, it is a tuple of two HTML elements. HTMX will take the elements and replace them where appropriate. As both have id specified (notifications and msg respectively), they will replace their predecessor on the page.",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "Web Devs Quickstart"
+ ]
+ },
+ {
+ "objectID": "tutorials/quickstart_for_web_devs.html#file-uploads",
+ "href": "tutorials/quickstart_for_web_devs.html#file-uploads",
+ "title": "Web Devs Quickstart",
+ "section": "File Uploads",
+ "text": "File Uploads\nA common task in web development is uploading files. This example is for uploading files to the hosting server, with information about the uploaded file presented to the user.\n\n\n\n\n\n\nFile uploads in production can be dangerous\n\n\n\nFile uploads can be the target of uabuse, accidental or intentional. That means users may attempt to upload files that are too large or present a security risk. This is especially of concern for public facing apps. File upload security is outside the scope of this tutorial, for now we suggest reading the OWASP File Upload Cheat Sheet.\n\n\n\nfrom fasthtml.common import *\nfrom pathlib import Path\n\napp, rt = fast_app()\n\nupload_dir = Path(\"filez\")\nupload_dir.mkdir(exist_ok=True)\n\n@rt('/')\ndef get():\n return Titled(\"File Upload Demo\",\n Div(cls='grid')(\n Article(\n1 Form(hx_post=\"/upload\", hx_target=\"#result-one\")(\n2 Input(type=\"file\", name=\"file\"),\n Button(\"Upload\", type=\"submit\", cls='secondary'),\n ),\n Div(id=\"result-one\")\n )\n )\n )\n\ndef FileMetaDataCard(file):\n return Article(\n Header(H3(file.filename)),\n Ul(\n Li('Size: ', file.size), \n Li('Content Type: ', file.content_type),\n Li('Headers: ', file.headers),\n )\n ) \n\n@rt('/upload')\n3async def post(file: UploadFile):\n4 card = FileMetaDataCard(file)\n5 filebuffer = await file.read()\n6 (upload_dir / file.filename).write_bytes(filebuffer)\n return card\n\nserve()\n\n\n1\n\nEvery form rendered with the Form FT component defaults to enctype=\"multipart/form-data\"\n\n2\n\nDon’t forget to set the Input FT Component’s type to file\n\n3\n\nThe upload view should receive a Starlette UploadFile type. You can add other form variables\n\n4\n\nWe can access the metadata of the card (filename, size, content_type, headers), a quick and safe process\n\n5\n\nIn order to access the contents contained within a file we use the await method to read() it. As files may be quite large or contain bad data, this is a seperate step from accessing metadata\n\n6\n\nThis step shows how to use Python’s built-in pathlib.Path library to write the file to disk.",
+ "crumbs": [
+ "Home",
+ "Tutorials",
+ "Web Devs Quickstart"
+ ]
+ },
+ {
+ "objectID": "unpublished/tutorial_for_web_devs.html",
+ "href": "unpublished/tutorial_for_web_devs.html",
+ "title": "BYO Blog",
+ "section": "",
+ "text": "Caution\n\n\n\nThis document is a work in progress.\nIn this tutorial we’re going to write a blog by example. Blogs are a good way to learn a web framework as they start simple yet can get surprisingly sophistated. The wikipedia definition of a blog is “an informational website consisting of discrete, often informal diary-style text entries (posts) informal diary-style text entries (posts)”, which means we need to provide these basic features:\nWe’ll also add in these features, so the blog can become a working site:"
+ },
+ {
+ "objectID": "unpublished/tutorial_for_web_devs.html#how-to-best-use-this-tutorial",
+ "href": "unpublished/tutorial_for_web_devs.html#how-to-best-use-this-tutorial",
+ "title": "BYO Blog",
+ "section": "How to best use this tutorial",
+ "text": "How to best use this tutorial\nWe could copy/paste every code example in sequence and have a finished blog at the end. However, it’s debatable how much we will learn through the copy/paste method. We’re not saying its impossible to learn through copy/paste, we’re just saying it’s not that of an efficient way to learn. It’s analogous to learning how to play a musical instrument or sport or video game by watching other people do it - you can learn some but its not the same as doing.\nA better approach is to type out every line of code in this tutorial. This forces us to run the code through our brains, giving us actual practice in how to write FastHTML and Pythoncode and forcing us to debug our own mistakes. In some cases we’ll repeat similar tasks - a key component in achieving mastery in anything. Coming back to the instrument/sport/video game analogy, it’s exactly like actually practicing an instrument, sport, or video game. Through practice and repetition we eventually achieve mastery."
+ },
+ {
+ "objectID": "unpublished/tutorial_for_web_devs.html#installing-fasthtml",
+ "href": "unpublished/tutorial_for_web_devs.html#installing-fasthtml",
+ "title": "BYO Blog",
+ "section": "Installing FastHTML",
+ "text": "Installing FastHTML\nFastHTML is just Python. Installation is often done with pip:\npip install python-fasthtml"
+ },
+ {
+ "objectID": "unpublished/tutorial_for_web_devs.html#a-minimal-fasthtml-app",
+ "href": "unpublished/tutorial_for_web_devs.html#a-minimal-fasthtml-app",
+ "title": "BYO Blog",
+ "section": "A minimal FastHTML app",
+ "text": "A minimal FastHTML app\nFirst, create the directory for our project using Python’s pathlib module:\nimport pathlib\npathlib.Path('blog-system').mkdir()\nNow that we have our directory, let’s create a minimal FastHTML site in it.\n\n\nblog-system/minimal.py\n\nfrom fasthtml.common import * \n\napp, rt = fast_app() \n\n@rt(\"/\") \ndef get():\n return Titled(\"FastHTML\", P(\"Let's do this!\")) \n\nserve()\n\nRun that with python minimal.py and you should get something like this:\npython minimal.py \nLink: http://localhost:5001\nINFO: Will watch for changes in these directories: ['/Users/pydanny/projects/blog-system']\nINFO: Uvicorn running on http://0.0.0.0:5001 (Press CTRL+C to quit)\nINFO: Started reloader process [46572] using WatchFiles\nINFO: Started server process [46576]\nINFO: Waiting for application startup.\nINFO: Application startup complete.\nConfirm FastHTML is running by opening your web browser to 127.0.0.1:5001. You should see something like the image below:\n\n\n\n\n\n\n\nWhat about the import *?\n\n\n\nFor those worried about the use of import * rather than a PEP8-style declared namespace, understand that __all__ is defined in FastHTML’s common module. That means that only the symbols (functions, classes, and other things) the framework wants us to have will be brought into our own code via import *. Read importing from a package) for more information.\nNevertheless, if we want to use a defined namespace we can do so. Here’s an example:\nfrom fasthtml import common as fh\n\n\napp, rt = fh.fast_app() \n\n@rt(\"/\") \ndef get():\n return fh.Titled(\"FastHTML\", fh.P(\"Let's do this!\")) \n\nfh.serve()"
+ },
+ {
+ "objectID": "unpublished/tutorial_for_web_devs.html#looking-more-closely-at-our-app",
+ "href": "unpublished/tutorial_for_web_devs.html#looking-more-closely-at-our-app",
+ "title": "BYO Blog",
+ "section": "Looking more closely at our app",
+ "text": "Looking more closely at our app\nLet’s look more closely at our application. Every line is packed with powerful features of FastHTML:\n\n\nblog-system/minimal.py\n\n1from fasthtml.common import *\n\n2app, rt = fast_app()\n\n3@rt(\"/\")\n4def get():\n5 return Titled(\"FastHTML\", P(\"Let's do this!\"))\n\n6serve()\n\n\n1\n\nThe top level namespace of Fast HTML (fasthtml.common) contains everything we need from FastHTML to build applications. A carefully-curated set of FastHTML functions and other Python objects is brought into our global namespace for convenience.\n\n2\n\nWe instantiate a FastHTML app with the fast_app() utility function. This provides a number of really useful defaults that we’ll modify or take advantage of later in the tutorial.\n\n3\n\nWe use the rt() decorator to tell FastHTML what to return when a user visits / in their browser.\n\n4\n\nWe connect this route to HTTP GET requests by defining a view function called get().\n\n5\n\nA tree of Python function calls that return all the HTML required to write a properly formed web page. You’ll soon see the power of this approach.\n\n6\n\nThe serve() utility configures and runs FastHTML using a library called uvicorn. Any changes to this module will be reloaded into the browser."
+ },
+ {
+ "objectID": "unpublished/tutorial_for_web_devs.html#adding-dynamic-content-to-our-minimal-app",
+ "href": "unpublished/tutorial_for_web_devs.html#adding-dynamic-content-to-our-minimal-app",
+ "title": "BYO Blog",
+ "section": "Adding dynamic content to our minimal app",
+ "text": "Adding dynamic content to our minimal app\nOur page is great, but we’ll make it better. Let’s add a randomized list of letters to the page. Every time the page reloads, a new list of varying length will be generated.\n\n\nblog-system/random_letters.py\n\nfrom fasthtml.common import *\n1import string, random\n\napp, rt = fast_app()\n\n@rt(\"/\")\ndef get():\n2 letters = random.choices(string.ascii_uppercase, k=random.randint(5, 20))\n3 items = [Li(c) for c in letters]\n return Titled(\"Random lists of letters\",\n4 Ul(*items)\n ) \n\nserve()\n\n\n1\n\nThe string and random libraries are part of Python’s standard library\n\n2\n\nWe use these libraries to generate a random length list of random letters called letters\n\n3\n\nUsing letters as the base we use list comprehension to generate a list of Li ft display components, each with their own letter and save that to the variable items\n\n4\n\nInside a call to the Ul() ft component we use Python’s *args special syntax on the items variable. Therefore *list is treated not as one argument but rather a set of them.\n\n\nWhen this is run, it will generate something like this with a different random list of letters for each page load:"
+ },
+ {
+ "objectID": "unpublished/tutorial_for_web_devs.html#storing-the-articles",
+ "href": "unpublished/tutorial_for_web_devs.html#storing-the-articles",
+ "title": "BYO Blog",
+ "section": "Storing the articles",
+ "text": "Storing the articles\nThe most basic component of a blog is a series of articles sorted by date authored. Rather than a database we’re going to use our computer’s harddrive to store a set of markdown files in a directory within our blog called posts. First, let’s create the directory and some test files we can use to search for:\n\nfrom fastcore.utils import *\n\n\n# Create some dummy posts\nposts = Path(\"posts\")\nposts.mkdir(exist_ok=True)\nfor i in range(10): (posts/f\"article_{i}.md\").write_text(f\"This is article {i}\")\n\nSearching for these files can be done with pathlib.\n\nimport pathlib\nposts.ls()\n\n(#10) [Path('posts/article_5.md'),Path('posts/article_1.md'),Path('posts/article_0.md'),Path('posts/article_4.md'),Path('posts/article_3.md'),Path('posts/article_7.md'),Path('posts/article_6.md'),Path('posts/article_2.md'),Path('posts/article_9.md'),Path('posts/article_8.md')]\n\n\n\n\n\n\n\n\nTip\n\n\n\nPython’s pathlib library is quite useful and makes file search and manipulation much easier. There’s many uses for it and is compatible across operating systems."
+ },
+ {
+ "objectID": "unpublished/tutorial_for_web_devs.html#creating-the-blog-home-page",
+ "href": "unpublished/tutorial_for_web_devs.html#creating-the-blog-home-page",
+ "title": "BYO Blog",
+ "section": "Creating the blog home page",
+ "text": "Creating the blog home page\nWe now have enough tools that we can create the home page. Let’s create a new Python file and write out our simple view to list the articles in our blog.\n\n\nblog-system/main.py\n\nfrom fasthtml.common import *\nimport pathlib\n\napp, rt = fast_app()\n\n@rt(\"/\")\ndef get():\n fnames = pathlib.Path(\"posts\").rglob(\"*.md\")\n items = [Li(A(fname, href=fname)) for fname in fnames] \n return Titled(\"My Blog\",\n Ul(*items)\n ) \n\nserve()\n\n\nfor p in posts.ls(): p.unlink()"
+ },
+ {
+ "objectID": "api/core.html",
+ "href": "api/core.html",
+ "title": "Core",
+ "section": "",
+ "text": "This is the source code to fasthtml. You won’t need to read this unless you want to understand how things are built behind the scenes, or need full details of a particular API. The notebook is converted to the Python module fasthtml/core.py using nbdev.",
+ "crumbs": [
+ "Home",
+ "Source",
+ "Core"
+ ]
+ },
+ {
+ "objectID": "api/core.html#imports-and-utils",
+ "href": "api/core.html#imports-and-utils",
+ "title": "Core",
+ "section": "Imports and utils",
+ "text": "Imports and utils\n\nimport time\n\nfrom IPython import display\nfrom enum import Enum\nfrom pprint import pprint\n\nfrom fastcore.test import *\nfrom starlette.testclient import TestClient\nfrom starlette.requests import Headers\nfrom starlette.datastructures import UploadFile\n\nWe write source code first, and then tests come after. The tests serve as both a means to confirm that the code works and also serves as working examples. The first declared function, date, is an example of this pattern.\n\nsource\n\nparsed_date\n\n parsed_date (s:str)\n\nConvert s to a datetime\n\nparsed_date('2pm')\n\ndatetime.datetime(2024, 10, 11, 14, 0)\n\n\n\nisinstance(date.fromtimestamp(0), date)\n\nTrue\n\n\n\nsource\n\n\nsnake2hyphens\n\n snake2hyphens (s:str)\n\nConvert s from snake case to hyphenated and capitalised\n\nsnake2hyphens(\"snake_case\")\n\n'Snake-Case'\n\n\n\nsource\n\n\nHtmxHeaders\n\n HtmxHeaders (boosted:str|None=None, current_url:str|None=None,\n history_restore_request:str|None=None, prompt:str|None=None,\n request:str|None=None, target:str|None=None,\n trigger_name:str|None=None, trigger:str|None=None)\n\n\ndef test_request(url: str='/', headers: dict={}, method: str='get') -> Request:\n scope = {\n 'type': 'http',\n 'method': method,\n 'path': url,\n 'headers': Headers(headers).raw,\n 'query_string': b'',\n 'scheme': 'http',\n 'client': ('127.0.0.1', 8000),\n 'server': ('127.0.0.1', 8000),\n }\n receive = lambda: {\"body\": b\"\", \"more_body\": False}\n return Request(scope, receive)\n\n\nh = test_request(headers=Headers({'HX-Request':'1'}))\n_get_htmx(h.headers)\n\nHtmxHeaders(boosted=None, current_url=None, history_restore_request=None, prompt=None, request='1', target=None, trigger_name=None, trigger=None)",
+ "crumbs": [
+ "Home",
+ "Source",
+ "Core"
+ ]
+ },
+ {
+ "objectID": "api/core.html#request-and-response",
+ "href": "api/core.html#request-and-response",
+ "title": "Core",
+ "section": "Request and response",
+ "text": "Request and response\n\ntest_eq(_fix_anno(Union[str,None])('a'), 'a')\ntest_eq(_fix_anno(float)(0.9), 0.9)\ntest_eq(_fix_anno(int)('1'), 1)\ntest_eq(_fix_anno(int)(['1','2']), 2)\ntest_eq(_fix_anno(list[int])(['1','2']), [1,2])\ntest_eq(_fix_anno(list[int])('1'), [1])\n\n\nd = dict(k=int, l=List[int])\ntest_eq(_form_arg('k', \"1\", d), 1)\ntest_eq(_form_arg('l', \"1\", d), [1])\ntest_eq(_form_arg('l', [\"1\",\"2\"], d), [1,2])\n\n\nsource\n\nHttpHeader\n\n HttpHeader (k:str, v:str)\n\n\n_to_htmx_header('trigger_after_settle')\n\n'HX-Trigger-After-Settle'\n\n\n\nsource\n\n\nHtmxResponseHeaders\n\n HtmxResponseHeaders (location=None, push_url=None, redirect=None,\n refresh=None, replace_url=None, reswap=None,\n retarget=None, reselect=None, trigger=None,\n trigger_after_settle=None, trigger_after_swap=None)\n\nHTMX response headers\n\nHtmxResponseHeaders(trigger_after_settle='hi')\n\nHttpHeader(k='HX-Trigger-After-Settle', v='hi')\n\n\n\nsource\n\n\nform2dict\n\n form2dict (form:starlette.datastructures.FormData)\n\nConvert starlette form data to a dict\n\nd = [('a',1),('a',2),('b',0)]\nfd = FormData(d)\nres = form2dict(fd)\ntest_eq(res['a'], [1,2])\ntest_eq(res['b'], 0)\n\n\nsource\n\n\nparse_form\n\n parse_form (req:starlette.requests.Request)\n\nStarlette errors on empty multipart forms, so this checks for that situation\n\nasync def f(req):\n def _f(p:HttpHeader): ...\n p = first(_sig(_f).parameters.values())\n result = await _from_body(req, p)\n return JSONResponse(result.__dict__)\n\napp = Starlette(routes=[Route('/', f, methods=['POST'])])\nclient = TestClient(app)\n\nd = dict(k='value1',v=['value2','value3'])\nresponse = client.post('/', data=d)\nprint(response.json())\n\n{'k': 'value1', 'v': 'value3'}\n\n\n\nasync def f(req): return Response(str(req.query_params.getlist('x')))\napp = Starlette(routes=[Route('/', f, methods=['GET'])])\nclient = TestClient(app)\nclient.get('/?x=1&x=2').text\n\n\"['1', '2']\"\n\n\n\ndef g(req, this:Starlette, a:str, b:HttpHeader): ...\n\nasync def f(req):\n a = await _wrap_req(req, _sig(g).parameters)\n return Response(str(a))\n\napp = Starlette(routes=[Route('/', f, methods=['POST'])])\nclient = TestClient(app)\n\nresponse = client.post('/?a=1', data=d)\nprint(response.text)\n\n[<starlette.requests.Request object>, <starlette.applications.Starlette object>, '1', HttpHeader(k='value1', v='value3')]\n\n\n\ndef g(req, this:Starlette, a:str, b:HttpHeader): ...\n\nasync def f(req):\n a = await _wrap_req(req, _sig(g).parameters)\n return Response(str(a))\n\napp = Starlette(routes=[Route('/', f, methods=['POST'])])\nclient = TestClient(app)\n\nresponse = client.post('/?a=1', data=d)\nprint(response.text)\n\n[<starlette.requests.Request object>, <starlette.applications.Starlette object>, '1', HttpHeader(k='value1', v='value3')]\n\n\n\nsource\n\n\nflat_xt\n\n flat_xt (lst)\n\nFlatten lists\n\nx = ft('a',1)\ntest_eq(flat_xt([x, x, [x,x]]), [x]*4)\ntest_eq(flat_xt(x), [x])\n\n\nsource\n\n\nBeforeware\n\n Beforeware (f, skip=None)\n\nInitialize self. See help(type(self)) for accurate signature.",
+ "crumbs": [
+ "Home",
+ "Source",
+ "Core"
+ ]
+ },
+ {
+ "objectID": "api/core.html#websockets-sse",
+ "href": "api/core.html#websockets-sse",
+ "title": "Core",
+ "section": "Websockets / SSE",
+ "text": "Websockets / SSE\n\ndef on_receive(self, msg:str): return f\"Message text was: {msg}\"\nc = _ws_endp(on_receive)\napp = Starlette(routes=[WebSocketRoute('/', _ws_endp(on_receive))])\n\ncli = TestClient(app)\nwith cli.websocket_connect('/') as ws:\n ws.send_text('{\"msg\":\"Hi!\"}')\n data = ws.receive_text()\n assert data == 'Message text was: Hi!'\n\n\nsource\n\nEventStream\n\n EventStream (s)\n\nCreate a text/event-stream response from s\n\nsource\n\n\nsignal_shutdown\n\n signal_shutdown ()",
+ "crumbs": [
+ "Home",
+ "Source",
+ "Core"
+ ]
+ },
+ {
+ "objectID": "api/core.html#routing-and-application",
+ "href": "api/core.html#routing-and-application",
+ "title": "Core",
+ "section": "Routing and application",
+ "text": "Routing and application\n\nsource\n\nWS_RouteX\n\n WS_RouteX (app, path:str, recv, conn:<built-infunctioncallable>=None,\n disconn:<built-infunctioncallable>=None, name=None,\n middleware=None)\n\nInitialize self. See help(type(self)) for accurate signature.\n\nsource\n\n\nuri\n\n uri (_arg, **kwargs)\n\n\nsource\n\n\ndecode_uri\n\n decode_uri (s)\n\n\nsource\n\n\nStringConvertor.to_string\n\n StringConvertor.to_string (value:str)\n\n\nsource\n\n\nHTTPConnection.url_path_for\n\n HTTPConnection.url_path_for (name:str, **path_params)\n\n\nsource\n\n\nflat_tuple\n\n flat_tuple (o)\n\nFlatten lists\n\nsource\n\n\nRedirect\n\n Redirect (loc)\n\nUse HTMX or Starlette RedirectResponse as required to redirect to loc\n\nsource\n\n\nRouteX\n\n RouteX (app, path:str, endpoint, methods=None, name=None,\n include_in_schema=True, middleware=None)\n\nInitialize self. See help(type(self)) for accurate signature.\n\nsource\n\n\nRouterX\n\n RouterX (app, routes=None, redirect_slashes=True, default=None,\n middleware=None)\n\nInitialize self. See help(type(self)) for accurate signature.\n\nsource\n\n\nget_key\n\n get_key (key=None, fname='.sesskey')\n\n\nget_key()\n\n'a604e4a2-08e8-462d-aff9-15468891fe09'\n\n\n\nsource\n\n\ndef_hdrs\n\n def_hdrs (htmx=True, ct_hdr=False, ws_hdr=False, surreal=True)\n\nDefault headers for a FastHTML app\n\nsource\n\n\nFastHTML\n\n FastHTML (debug=False, routes=None, middleware=None,\n exception_handlers=None, on_startup=None, on_shutdown=None,\n lifespan=None, hdrs=None, ftrs=None, before=None, after=None,\n ws_hdr=False, ct_hdr=False, surreal=True, htmx=True,\n default_hdrs=True, sess_cls=<class\n 'starlette.middleware.sessions.SessionMiddleware'>,\n secret_key=None, session_cookie='session_', max_age=31536000,\n sess_path='/', same_site='lax', sess_https_only=False,\n sess_domain=None, key_fname='.sesskey', htmlkw=None, **bodykw)\n\n*Creates an application instance.\nParameters:\n\ndebug - Boolean indicating if debug tracebacks should be returned on errors.\nroutes - A list of routes to serve incoming HTTP and WebSocket requests.\nmiddleware - A list of middleware to run for every request. A starlette application will always automatically include two middleware classes. ServerErrorMiddleware is added as the very outermost middleware, to handle any uncaught errors occurring anywhere in the entire stack. ExceptionMiddleware is added as the very innermost middleware, to deal with handled exception cases occurring in the routing or endpoints.\nexception_handlers - A mapping of either integer status codes, or exception class types onto callables which handle the exceptions. Exception handler callables should be of the form handler(request, exc) -> response and may be either standard functions, or async functions.\non_startup - A list of callables to run on application startup. Startup handler callables do not take any arguments, and may be either standard functions, or async functions.\non_shutdown - A list of callables to run on application shutdown. Shutdown handler callables do not take any arguments, and may be either standard functions, or async functions.\nlifespan - A lifespan context function, which can be used to perform startup and shutdown tasks. This is a newer style that replaces the on_startup and on_shutdown handlers. Use one or the other, not both.*\n\n\nsource\n\n\nFastHTML.route\n\n FastHTML.route (path:str=None, methods=None, name=None,\n include_in_schema=True)\n\nAdd a route at path\n\nsource\n\n\nserve\n\n serve (appname=None, app='app', host='0.0.0.0', port=None, reload=True,\n reload_includes:list[str]|str|None=None,\n reload_excludes:list[str]|str|None=None)\n\nRun the app in an async server, with live reload set as the default.\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nappname\nNoneType\nNone\nName of the module\n\n\napp\nstr\napp\nApp instance to be served\n\n\nhost\nstr\n0.0.0.0\nIf host is 0.0.0.0 will convert to localhost\n\n\nport\nNoneType\nNone\nIf port is None it will default to 5001 or the PORT environment variable\n\n\nreload\nbool\nTrue\nDefault is to reload the app upon code changes\n\n\nreload_includes\nlist[str] | str | None\nNone\nAdditional files to watch for changes\n\n\nreload_excludes\nlist[str] | str | None\nNone\nFiles to ignore for changes\n\n\n\n\nsource\n\n\nClient\n\n Client (app, url='http://testserver')\n\nA simple httpx ASGI client that doesn’t require async\n\napp = FastHTML(routes=[Route('/', lambda _: Response('test'))])\ncli = Client(app)\n\ncli.get('/').text\n\n'test'\n\n\nNote that you can also use Starlette’s TestClient instead of FastHTML’s Client. They should be largely interchangable.",
+ "crumbs": [
+ "Home",
+ "Source",
+ "Core"
+ ]
+ },
+ {
+ "objectID": "api/core.html#fasthtml-tests",
+ "href": "api/core.html#fasthtml-tests",
+ "title": "Core",
+ "section": "FastHTML Tests",
+ "text": "FastHTML Tests\n\ndef get_cli(app): return app,TestClient(app),app.route\n\n\napp,cli,rt = get_cli(FastHTML(secret_key='soopersecret'))\n\n\n@rt(\"/hi\")\ndef get(): return 'Hi there'\n\nr = cli.get('/hi')\nr.text\n\n'Hi there'\n\n\n\n@rt(\"/hi\")\ndef post(): return 'Postal'\n\ncli.post('/hi').text\n\n'Postal'\n\n\n\n@app.get(\"/hostie\")\ndef show_host(req): return req.headers['host']\n\ncli.get('/hostie').text\n\n'testserver'\n\n\n\n@rt\ndef yoyo(): return 'a yoyo'\n\ncli.post('/yoyo').text\n\n'a yoyo'\n\n\n\n@app.get\ndef autopost(): return Html(Div('Text.', hx_post=yoyo()))\nprint(cli.get('/autopost').text)\n\n <!doctype html>\n <html>\n <div hx-post=\"a yoyo\">Text.</div>\n </html>\n\n\n\n\n@app.get\ndef autopost2(): return Html(Body(Div('Text.', cls='px-2', hx_post=show_host.rt(a='b'))))\nprint(cli.get('/autopost2').text)\n\n <!doctype html>\n <html>\n <body>\n <div class=\"px-2\" hx-post=\"/hostie?a=b\">Text.</div>\n </body>\n </html>\n\n\n\n\n@app.get\ndef autoget2(): return Html(Div('Text.', hx_get=show_host))\nprint(cli.get('/autoget2').text)\n\n <!doctype html>\n <html>\n <div hx-get=\"/hostie\">Text.</div>\n </html>\n\n\n\n\n@rt('/user/{nm}', name='gday')\ndef get(nm:str=''): return f\"Good day to you, {nm}!\"\ncli.get('/user/Alexis').text\n\n'Good day to you, Alexis!'\n\n\n\n@app.get\ndef autolink(): return Html(Div('Text.', link=uri('gday', nm='Alexis')))\nprint(cli.get('/autolink').text)\n\n <!doctype html>\n <html>\n <div href=\"/user/Alexis\">Text.</div>\n </html>\n\n\n\n\n@rt('/link')\ndef get(req): return f\"{req.url_for('gday', nm='Alexis')}; {req.url_for('show_host')}\"\n\ncli.get('/link').text\n\n'http://testserver/user/Alexis; http://testserver/hostie'\n\n\n\n@app.get(\"/background\")\nasync def background_task(request):\n async def long_running_task():\n await asyncio.sleep(0.1)\n print(\"Background task completed!\")\n return P(\"Task started\"), BackgroundTask(long_running_task)\n\nresponse = cli.get(\"/background\")\n\nBackground task completed!\n\n\n\ntest_eq(app.router.url_path_for('gday', nm='Jeremy'), '/user/Jeremy')\n\n\nhxhdr = {'headers':{'hx-request':\"1\"}}\n\n@rt('/ft')\ndef get(): return Title('Foo'),H1('bar')\n\ntxt = cli.get('/ft').text\nassert '<title>Foo</title>' in txt and '<h1>bar</h1>' in txt and '<html>' in txt\n\n@rt('/xt2')\ndef get(): return H1('bar')\n\ntxt = cli.get('/xt2').text\nassert '<title>FastHTML page</title>' in txt and '<h1>bar</h1>' in txt and '<html>' in txt\n\nassert cli.get('/xt2', **hxhdr).text.strip() == '<h1>bar</h1>'\n\n@rt('/xt3')\ndef get(): return Html(Head(Title('hi')), Body(P('there')))\n\ntxt = cli.get('/xt3').text\nassert '<title>FastHTML page</title>' not in txt and '<title>hi</title>' in txt and '<p>there</p>' in txt\n\n\n@rt('/oops')\ndef get(nope): return nope\ntest_warns(lambda: cli.get('/oops?nope=1'))\n\n\ndef test_r(cli, path, exp, meth='get', hx=False, **kwargs):\n if hx: kwargs['headers'] = {'hx-request':\"1\"}\n test_eq(getattr(cli, meth)(path, **kwargs).text, exp)\n\nModelName = str_enum('ModelName', \"alexnet\", \"resnet\", \"lenet\")\nfake_db = [{\"name\": \"Foo\"}, {\"name\": \"Bar\"}]\n\n\n@rt('/html/{idx}')\nasync def get(idx:int): return Body(H4(f'Next is {idx+1}.'))\n\n\n@rt(\"/models/{nm}\")\ndef get(nm:ModelName): return nm\n\n@rt(\"/files/{path}\")\nasync def get(path: Path): return path.with_suffix('.txt')\n\n@rt(\"/items/\")\ndef get(idx:int|None = 0): return fake_db[idx]\n\n@rt(\"/idxl/\")\ndef get(idx:list[int]): return str(idx)\n\n\nr = cli.get('/html/1', headers={'hx-request':\"1\"})\nassert '<h4>Next is 2.</h4>' in r.text\ntest_r(cli, '/models/alexnet', 'alexnet')\ntest_r(cli, '/files/foo', 'foo.txt')\ntest_r(cli, '/items/?idx=1', '{\"name\":\"Bar\"}')\ntest_r(cli, '/items/', '{\"name\":\"Foo\"}')\nassert cli.get('/items/?idx=g').text=='404 Not Found'\nassert cli.get('/items/?idx=g').status_code == 404\ntest_r(cli, '/idxl/?idx=1&idx=2', '[1, 2]')\nassert cli.get('/idxl/?idx=1&idx=g').status_code == 404\n\n\napp = FastHTML()\nrt = app.route\ncli = TestClient(app)\n@app.route(r'/static/{path:path}.jpg')\ndef index(path:str): return f'got {path}'\ncli.get('/static/sub/a.b.jpg').text\n\n'got sub/a.b'\n\n\n\napp.chk = 'foo'\n\n\n@app.get(\"/booly/\")\ndef _(coming:bool=True): return 'Coming' if coming else 'Not coming'\n\n@app.get(\"/datie/\")\ndef _(d:parsed_date): return d\n\n@app.get(\"/ua\")\nasync def _(user_agent:str): return user_agent\n\n@app.get(\"/hxtest\")\ndef _(htmx): return htmx.request\n\n@app.get(\"/hxtest2\")\ndef _(foo:HtmxHeaders, req): return foo.request\n\n@app.get(\"/app\")\ndef _(app): return app.chk\n\n@app.get(\"/app2\")\ndef _(foo:FastHTML): return foo.chk,HttpHeader(\"mykey\", \"myval\")\n\n@app.get(\"/app3\")\ndef _(foo:FastHTML): return HtmxResponseHeaders(location=\"http://example.org\")\n\n@app.get(\"/app4\")\ndef _(foo:FastHTML): return Redirect(\"http://example.org\")\n\n\ntest_r(cli, '/booly/?coming=true', 'Coming')\ntest_r(cli, '/booly/?coming=no', 'Not coming')\ndate_str = \"17th of May, 2024, 2p\"\ntest_r(cli, f'/datie/?d={date_str}', '2024-05-17 14:00:00')\ntest_r(cli, '/ua', 'FastHTML', headers={'User-Agent':'FastHTML'})\ntest_r(cli, '/hxtest' , '1', headers={'HX-Request':'1'})\ntest_r(cli, '/hxtest2', '1', headers={'HX-Request':'1'})\ntest_r(cli, '/app' , 'foo')\n\n\nr = cli.get('/app2', **hxhdr)\ntest_eq(r.text, 'foo')\ntest_eq(r.headers['mykey'], 'myval')\n\n\nr = cli.get('/app3')\ntest_eq(r.headers['HX-Location'], 'http://example.org')\n\n\nr = cli.get('/app4', follow_redirects=False)\ntest_eq(r.status_code, 303)\n\n\nr = cli.get('/app4', headers={'HX-Request':'1'})\ntest_eq(r.headers['HX-Redirect'], 'http://example.org')\n\n\n@rt\ndef meta(): \n return ((Title('hi'),H1('hi')),\n (Meta(property='image'), Meta(property='site_name'))\n )\n\nt = cli.post('/meta').text\nassert re.search(r'<body>\\s*<h1>hi</h1>\\s*</body>', t)\nassert '<meta' in t\n\n\n@app.post('/profile/me')\ndef profile_update(username: str): return username\n\ntest_r(cli, '/profile/me', 'Alexis', 'post', data={'username' : 'Alexis'})\ntest_r(cli, '/profile/me', 'Missing required field: username', 'post', data={})\n\n\n# Example post request with parameter that has a default value\n@app.post('/pet/dog')\ndef pet_dog(dogname: str = None): return dogname\n\n# Working post request with optional parameter\ntest_r(cli, '/pet/dog', '', 'post', data={})\n\n\n@dataclass\nclass Bodie: a:int;b:str\n\n@rt(\"/bodie/{nm}\")\ndef post(nm:str, data:Bodie):\n res = asdict(data)\n res['nm'] = nm\n return res\n\n@app.post(\"/bodied/\")\ndef bodied(data:dict): return data\n\nnt = namedtuple('Bodient', ['a','b'])\n\n@app.post(\"/bodient/\")\ndef bodient(data:nt): return asdict(data)\n\nclass BodieTD(TypedDict): a:int;b:str='foo'\n\n@app.post(\"/bodietd/\")\ndef bodient(data:BodieTD): return data\n\nclass Bodie2:\n a:int|None; b:str\n def __init__(self, a, b='foo'): store_attr()\n\n@rt(\"/bodie2/\", methods=['get','post'])\ndef bodie(d:Bodie2): return f\"a: {d.a}; b: {d.b}\"\n\n\nfrom fasthtml.xtend import Titled\n\n\nd = dict(a=1, b='foo')\n\ntest_r(cli, '/bodie/me', '{\"a\":1,\"b\":\"foo\",\"nm\":\"me\"}', 'post', data=dict(a=1, b='foo', nm='me'))\ntest_r(cli, '/bodied/', '{\"a\":\"1\",\"b\":\"foo\"}', 'post', data=d)\ntest_r(cli, '/bodie2/', 'a: 1; b: foo', 'post', data={'a':1})\ntest_r(cli, '/bodie2/?a=1&b=foo&nm=me', 'a: 1; b: foo')\ntest_r(cli, '/bodient/', '{\"a\":\"1\",\"b\":\"foo\"}', 'post', data=d)\ntest_r(cli, '/bodietd/', '{\"a\":1,\"b\":\"foo\"}', 'post', data=d)\n\n\n# Testing POST with Content-Type: application/json\n@app.post(\"/\")\ndef index(it: Bodie): return Titled(\"It worked!\", P(f\"{it.a}, {it.b}\"))\n\ns = json.dumps({\"b\": \"Lorem\", \"a\": 15})\nresponse = cli.post('/', headers={\"Content-Type\": \"application/json\"}, data=s).text\nassert \"<title>It worked!</title>\" in response and \"<p>15, Lorem</p>\" in response\n\n\n# Testing POST with Content-Type: application/json\n@app.post(\"/bodytext\")\ndef index(body): return body\n\nresponse = cli.post('/bodytext', headers={\"Content-Type\": \"application/json\"}, data=s).text\ntest_eq(response, '{\"b\": \"Lorem\", \"a\": 15}')\n\n\nfiles = [ ('files', ('file1.txt', b'content1')),\n ('files', ('file2.txt', b'content2')) ]\n\n\n@rt(\"/uploads\")\nasync def post(files:list[UploadFile]):\n return ','.join([(await file.read()).decode() for file in files])\n\nres = cli.post('/uploads', files=files) \nprint(res.status_code)\nprint(res.text)\n\n200\ncontent1,content2\n\n\n\n@rt(\"/setsess\")\ndef get(sess, foo:str=''):\n now = datetime.now()\n sess['auth'] = str(now)\n return f'Set to {now}'\n\n@rt(\"/getsess\")\ndef get(sess): return f'Session time: {sess[\"auth\"]}'\n\nprint(cli.get('/setsess').text)\ntime.sleep(0.01)\n\ncli.get('/getsess').text\n\nSet to 2024-10-11 09:46:57.944986\n\n\n'Session time: 2024-10-11 09:46:57.944986'\n\n\n\n@rt(\"/sess-first\")\ndef post(sess, name: str):\n sess[\"name\"] = name\n return str(sess)\n\ncli.post('/sess-first', data={'name': 2})\n\n@rt(\"/getsess-all\")\ndef get(sess): return sess['name']\n\ntest_eq(cli.get('/getsess-all').text, '2')\n\n\n@rt(\"/upload\")\nasync def post(uf:UploadFile): return (await uf.read()).decode()\n\nwith open('../../CHANGELOG.md', 'rb') as f:\n print(cli.post('/upload', files={'uf':f}, data={'msg':'Hello'}).text[:15])\n\n# Release notes\n\n\n\n@rt(\"/form-submit/{list_id}\")\ndef options(list_id: str):\n headers = {\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Methods': 'POST',\n 'Access-Control-Allow-Headers': '*',\n }\n return Response(status_code=200, headers=headers)\n\n\nh = cli.options('/form-submit/2').headers\ntest_eq(h['Access-Control-Allow-Methods'], 'POST')\n\n\nfrom fasthtml.authmw import user_pwd_auth\n\n\ndef _not_found(req, exc): return Div('nope')\n\napp,cli,rt = get_cli(FastHTML(exception_handlers={404:_not_found}))\n\ntxt = cli.get('/').text\nassert '<div>nope</div>' in txt\nassert '<!doctype html>' in txt\n\n\napp,cli,rt = get_cli(FastHTML())\n\n@rt(\"/{name}/{age}\")\ndef get(name: str, age: int): \n return Titled(f\"Hello {name.title()}, age {age}\")\n\nassert '<title>Hello Uma, age 5</title>' in cli.get('/uma/5').text\nassert '404 Not Found' in cli.get('/uma/five').text\n\n\nauth = user_pwd_auth(testuser='spycraft')\napp,cli,rt = get_cli(FastHTML(middleware=[auth]))\n\n@rt(\"/locked\")\ndef get(auth): return 'Hello, ' + auth\n\ntest_eq(cli.get('/locked').text, 'not authenticated')\ntest_eq(cli.get('/locked', auth=(\"testuser\",\"spycraft\")).text, 'Hello, testuser')\n\n\nauth = user_pwd_auth(testuser='spycraft')\napp,cli,rt = get_cli(FastHTML(middleware=[auth]))\n\n@rt(\"/locked\")\ndef get(auth): return 'Hello, ' + auth\n\ntest_eq(cli.get('/locked').text, 'not authenticated')\ntest_eq(cli.get('/locked', auth=(\"testuser\",\"spycraft\")).text, 'Hello, testuser')",
+ "crumbs": [
+ "Home",
+ "Source",
+ "Core"
+ ]
+ },
+ {
+ "objectID": "api/core.html#extras",
+ "href": "api/core.html#extras",
+ "title": "Core",
+ "section": "Extras",
+ "text": "Extras\n\napp,cli,rt = get_cli(FastHTML(secret_key='soopersecret'))\n\n\nsource\n\ncookie\n\n cookie (key:str, value='', max_age=None, expires=None, path='/',\n domain=None, secure=False, httponly=False, samesite='lax')\n\nCreate a ‘set-cookie’ HttpHeader\n\n@rt(\"/setcookie\")\ndef get(req): return cookie('now', datetime.now())\n\n@rt(\"/getcookie\")\ndef get(now:parsed_date): return f'Cookie was set at time {now.time()}'\n\nprint(cli.get('/setcookie').text)\ntime.sleep(0.01)\ncli.get('/getcookie').text\n\n\n\n\n'Cookie was set at time 09:46:58.348630'\n\n\n\nsource\n\n\nreg_re_param\n\n reg_re_param (m, s)\n\n\nsource\n\n\nFastHTML.static_route_exts\n\n FastHTML.static_route_exts (prefix='/', static_path='.', exts='static')\n\nAdd a static route at URL path prefix with files from static_path and exts defined by reg_re_param()\n\nreg_re_param(\"imgext\", \"ico|gif|jpg|jpeg|webm\")\n\n@rt(r'/static/{path:path}{fn}.{ext:imgext}')\ndef get(fn:str, path:str, ext:str): return f\"Getting {fn}.{ext} from /{path}\"\n\ntest_r(cli, '/static/foo/jph.me.ico', 'Getting jph.me.ico from /foo/')\n\n\napp.static_route_exts()\nassert 'These are the source notebooks for FastHTML' in cli.get('/README.txt').text\n\n\nsource\n\n\nFastHTML.static_route\n\n FastHTML.static_route (ext='', prefix='/', static_path='.')\n\nAdd a static route at URL path prefix with files from static_path and single ext (including the ‘.’)\n\napp.static_route('.md', static_path='../..')\nassert 'THIS FILE WAS AUTOGENERATED' in cli.get('/README.md').text\n\n\nsource\n\n\nMiddlewareBase\n\n MiddlewareBase ()\n\nInitialize self. See help(type(self)) for accurate signature.\n\nsource\n\n\nFtResponse\n\n FtResponse (content, status_code:int=200, headers=None, cls=<class\n 'starlette.responses.HTMLResponse'>,\n media_type:str|None=None)\n\nWrap an FT response with any Starlette Response\n\n@rt('/ftr')\ndef get():\n cts = Title('Foo'),H1('bar')\n return FtResponse(cts, status_code=201, headers={'Location':'/foo/1'})\n\nr = cli.get('/ftr')\n\ntest_eq(r.status_code, 201)\ntest_eq(r.headers['location'], '/foo/1')\ntxt = r.text\nassert '<title>Foo</title>' in txt and '<h1>bar</h1>' in txt and '<html>' in txt\n\n\nsource\n\n\nunqid\n\n unqid ()\n\n\nsource\n\n\nPusher\n\n Pusher (app, dest_id='_dest', auto_id=True)\n\nInitialize self. See help(type(self)) for accurate signature.",
+ "crumbs": [
+ "Home",
+ "Source",
+ "Core"
+ ]
+ },
+ {
+ "objectID": "api/cli.html",
+ "href": "api/cli.html",
+ "title": "Command Line Tools",
+ "section": "",
+ "text": "source\n\nrailway_link\n\n railway_link ()\n\nLink the current directory to the current project’s Railway service\n\nsource\n\n\nrailway_deploy\n\n railway_deploy (name:str, mount:<function bool_arg>=True)\n\nDeploy a FastHTML app to Railway\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nname\nstr\n\nThe project name to deploy\n\n\nmount\nbool_arg\nTrue\nCreate a mounted volume at /app/data?",
+ "crumbs": [
+ "Home",
+ "Source",
+ "Command Line Tools"
+ ]
+ },
+ {
+ "objectID": "api/svg.html",
+ "href": "api/svg.html",
+ "title": "SVG",
+ "section": "",
+ "text": "from nbdev.showdoc import show_doc\nYou can create SVGs directly from strings, for instance (as always, use NotStr or Safe to tell FastHTML to not escape the text):\nsvg = '<svg width=\"50\" height=\"50\"><circle cx=\"20\" cy=\"20\" r=\"15\" fill=\"red\"></circle></svg>'\nshow(NotStr(svg))\nYou can also use libraries such as fa6-icons.\nTo create and modify SVGs using a Python API, use the FT elements in fasthtml.svg, discussed below.\nNote: fasthtml.common does NOT automatically export SVG elements. To get access to them, you need to import fasthtml.svg like so\nfrom fasthtml.svg import *\nsource",
+ "crumbs": [
+ "Home",
+ "Source",
+ "SVG"
+ ]
+ },
+ {
+ "objectID": "api/svg.html#basic-shapes",
+ "href": "api/svg.html#basic-shapes",
+ "title": "SVG",
+ "section": "Basic shapes",
+ "text": "Basic shapes\nWe’ll define a simple function to display SVG shapes in this notebook:\n\ndef demo(el, h=50, w=50): return show(Svg(h=h,w=w)(el))\n\n\nsource\n\nRect\n\n Rect (width, height, x=0, y=0, fill=None, stroke=None, stroke_width=None,\n rx=None, ry=None, transform=None, opacity=None, clip=None,\n mask=None, filter=None, vector_effect=None, pointer_events=None,\n target_id=None, hx_vals=None, id=None, cls=None, title=None,\n style=None, accesskey=None, contenteditable=None, dir=None,\n draggable=None, enterkeyhint=None, hidden=None, inert=None,\n inputmode=None, lang=None, popover=None, spellcheck=None,\n tabindex=None, translate=None, hx_get=None, hx_post=None,\n hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,\n hx_target=None, hx_swap=None, hx_swap_oob=None, hx_include=None,\n hx_select=None, hx_select_oob=None, hx_indicator=None,\n hx_push_url=None, hx_confirm=None, hx_disable=None,\n hx_replace_url=None, hx_disabled_elt=None, hx_ext=None,\n hx_headers=None, hx_history=None, hx_history_elt=None,\n hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None)\n\nA standard SVG rect element\nAll our shapes just create regular FT elements. The only extra functionality provided by most of them is to add additional defined kwargs to improve auto-complete in IDEs and notebooks, and re-order parameters so that positional args can also be used to save a bit of typing, e.g:\n\ndemo(Rect(30, 30, fill='blue', rx=8, ry=8))\n\n\n\n\n\nsource\n\n\nCircle\n\n Circle (r, cx=0, cy=0, fill=None, stroke=None, stroke_width=None,\n transform=None, opacity=None, clip=None, mask=None, filter=None,\n vector_effect=None, pointer_events=None, target_id=None,\n hx_vals=None, id=None, cls=None, title=None, style=None,\n accesskey=None, contenteditable=None, dir=None, draggable=None,\n enterkeyhint=None, hidden=None, inert=None, inputmode=None,\n lang=None, popover=None, spellcheck=None, tabindex=None,\n translate=None, hx_get=None, hx_post=None, hx_put=None,\n hx_delete=None, hx_patch=None, hx_trigger=None, hx_target=None,\n hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None)\n\nA standard SVG circle element\n\ndemo(Circle(20, 25, 25, stroke='red', stroke_width=3))\n\n\n\n\n\nsource\n\n\nEllipse\n\n Ellipse (rx, ry, cx=0, cy=0, fill=None, stroke=None, stroke_width=None,\n transform=None, opacity=None, clip=None, mask=None, filter=None,\n vector_effect=None, pointer_events=None, target_id=None,\n hx_vals=None, id=None, cls=None, title=None, style=None,\n accesskey=None, contenteditable=None, dir=None, draggable=None,\n enterkeyhint=None, hidden=None, inert=None, inputmode=None,\n lang=None, popover=None, spellcheck=None, tabindex=None,\n translate=None, hx_get=None, hx_post=None, hx_put=None,\n hx_delete=None, hx_patch=None, hx_trigger=None, hx_target=None,\n hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None)\n\nA standard SVG ellipse element\n\ndemo(Ellipse(20, 10, 25, 25))\n\n\n\n\n\nsource\n\n\ntransformd\n\n transformd (translate=None, scale=None, rotate=None, skewX=None,\n skewY=None, matrix=None)\n\nCreate an SVG transform kwarg dict\n\nrot = transformd(rotate=(45, 25, 25))\nrot\n\n{'transform': 'rotate(45,25,25)'}\n\n\n\ndemo(Ellipse(20, 10, 25, 25, **rot))\n\n\n\n\n\nsource\n\n\nLine\n\n Line (x1, y1, x2=0, y2=0, stroke='black', w=None, stroke_width=1,\n transform=None, opacity=None, clip=None, mask=None, filter=None,\n vector_effect=None, pointer_events=None, target_id=None,\n hx_vals=None, id=None, cls=None, title=None, style=None,\n accesskey=None, contenteditable=None, dir=None, draggable=None,\n enterkeyhint=None, hidden=None, inert=None, inputmode=None,\n lang=None, popover=None, spellcheck=None, tabindex=None,\n translate=None, hx_get=None, hx_post=None, hx_put=None,\n hx_delete=None, hx_patch=None, hx_trigger=None, hx_target=None,\n hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,\n hx_sync=None, hx_validate=None)\n\nA standard SVG line element\n\ndemo(Line(20, 30, w=3))\n\n\n\n\n\nsource\n\n\nPolyline\n\n Polyline (*args, points=None, fill=None, stroke=None, stroke_width=None,\n transform=None, opacity=None, clip=None, mask=None,\n filter=None, vector_effect=None, pointer_events=None,\n target_id=None, hx_vals=None, id=None, cls=None, title=None,\n style=None, accesskey=None, contenteditable=None, dir=None,\n draggable=None, enterkeyhint=None, hidden=None, inert=None,\n inputmode=None, lang=None, popover=None, spellcheck=None,\n tabindex=None, translate=None, hx_get=None, hx_post=None,\n hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,\n hx_target=None, hx_swap=None, hx_swap_oob=None,\n hx_include=None, hx_select=None, hx_select_oob=None,\n hx_indicator=None, hx_push_url=None, hx_confirm=None,\n hx_disable=None, hx_replace_url=None, hx_disabled_elt=None,\n hx_ext=None, hx_headers=None, hx_history=None,\n hx_history_elt=None, hx_inherit=None, hx_params=None,\n hx_preserve=None, hx_prompt=None, hx_request=None,\n hx_sync=None, hx_validate=None)\n\nA standard SVG polyline element\n\ndemo(Polyline((0,0), (10,10), (20,0), (30,10), (40,0),\n fill='yellow', stroke='blue', stroke_width=2))\n\n\n\n\n\ndemo(Polyline(points='0,0 10,10 20,0 30,10 40,0', fill='purple', stroke_width=2))\n\n\n\n\n\nsource\n\n\nPolygon\n\n Polygon (*args, points=None, fill=None, stroke=None, stroke_width=None,\n transform=None, opacity=None, clip=None, mask=None, filter=None,\n vector_effect=None, pointer_events=None, target_id=None,\n hx_vals=None, id=None, cls=None, title=None, style=None,\n accesskey=None, contenteditable=None, dir=None, draggable=None,\n enterkeyhint=None, hidden=None, inert=None, inputmode=None,\n lang=None, popover=None, spellcheck=None, tabindex=None,\n translate=None, hx_get=None, hx_post=None, hx_put=None,\n hx_delete=None, hx_patch=None, hx_trigger=None, hx_target=None,\n hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None)\n\nA standard SVG polygon element\n\ndemo(Polygon((25,5), (43.3,15), (43.3,35), (25,45), (6.7,35), (6.7,15), \n fill='lightblue', stroke='navy', stroke_width=2))\n\n\n\n\n\ndemo(Polygon(points='25,5 43.3,15 43.3,35 25,45 6.7,35 6.7,15',\n fill='lightgreen', stroke='darkgreen', stroke_width=2))\n\n\n\n\n\nsource\n\n\nText\n\n Text (*args, x=0, y=0, font_family=None, font_size=None, fill=None,\n text_anchor=None, dominant_baseline=None, font_weight=None,\n font_style=None, text_decoration=None, transform=None,\n opacity=None, clip=None, mask=None, filter=None,\n vector_effect=None, pointer_events=None, target_id=None,\n hx_vals=None, id=None, cls=None, title=None, style=None,\n accesskey=None, contenteditable=None, dir=None, draggable=None,\n enterkeyhint=None, hidden=None, inert=None, inputmode=None,\n lang=None, popover=None, spellcheck=None, tabindex=None,\n translate=None, hx_get=None, hx_post=None, hx_put=None,\n hx_delete=None, hx_patch=None, hx_trigger=None, hx_target=None,\n hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,\n hx_sync=None, hx_validate=None)\n\nA standard SVG text element\n\ndemo(Text(\"Hello!\", x=10, y=30))\n\nHello!",
+ "crumbs": [
+ "Home",
+ "Source",
+ "SVG"
+ ]
+ },
+ {
+ "objectID": "api/svg.html#paths",
+ "href": "api/svg.html#paths",
+ "title": "SVG",
+ "section": "Paths",
+ "text": "Paths\nPaths in SVGs are more complex, so we add a small (optional) fluent interface for constructing them:\n\nsource\n\nPathFT\n\n PathFT (tag:str, cs:tuple, attrs:dict=None, void_=False, **kwargs)\n\nA ‘Fast Tag’ structure, containing tag,children,and attrs\n\nsource\n\n\nPath\n\n Path (d='', fill=None, stroke=None, stroke_width=None, transform=None,\n opacity=None, clip=None, mask=None, filter=None,\n vector_effect=None, pointer_events=None, target_id=None,\n hx_vals=None, id=None, cls=None, title=None, style=None,\n accesskey=None, contenteditable=None, dir=None, draggable=None,\n enterkeyhint=None, hidden=None, inert=None, inputmode=None,\n lang=None, popover=None, spellcheck=None, tabindex=None,\n translate=None, hx_get=None, hx_post=None, hx_put=None,\n hx_delete=None, hx_patch=None, hx_trigger=None, hx_target=None,\n hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,\n hx_sync=None, hx_validate=None)\n\nCreate a standard path SVG element. This is a special object\nLet’s create a square shape, but using Path instead of Rect:\n\nM(10, 10): Move to starting point (10, 10)\nL(40, 10): Line to (40, 10) - top edge\nL(40, 40): Line to (40, 40) - right edge\nL(10, 40): Line to (10, 40) - bottom edge\nZ(): Close path - connects back to start\n\nM = Move to, L = Line to, Z = Close path\n\ndemo(Path(fill='none', stroke='purple', stroke_width=2\n ).M(10, 10).L(40, 10).L(40, 40).L(10, 40).Z())\n\n\n\n\nUsing curves we can create a spiral:\n\np = (Path(fill='none', stroke='purple', stroke_width=2)\n .M(25, 25)\n .C(25, 25, 20, 20, 30, 20)\n .C(40, 20, 40, 30, 30, 30)\n .C(20, 30, 20, 15, 35, 15)\n .C(50, 15, 50, 35, 25, 35)\n .C(0, 35, 0, 10, 40, 10)\n .C(80, 10, 80, 40, 25, 40))\ndemo(p, 50, 100)\n\n\n\n\nUsing arcs and curves we can create a map marker icon:\n\np = (Path(fill='red')\n .M(25,45)\n .C(25,45,10,35,10,25)\n .A(15,15,0,1,1,40,25)\n .C(40,35,25,45,25,45)\n .Z())\ndemo(p)\n\n\n\n\nBehind the scenes it’s just creating regular SVG path d attr – you can pass d in directly if you prefer.\n\nprint(p.d)\n\nM25 45 C25 45 10 35 10 25 A15 15 0 1 1 40 25 C40 35 25 45 25 45 Z\n\n\n\ndemo(Path(d='M25 45 C25 45 10 35 10 25 A15 15 0 1 1 40 25 C40 35 25 45 25 45 Z'))\n\n\n\n\n\nsource\n\n\nPathFT.M\n\n PathFT.M (x, y)\n\nMove to.\n\nsource\n\n\nPathFT.L\n\n PathFT.L (x, y)\n\nLine to.\n\nsource\n\n\nPathFT.H\n\n PathFT.H (x)\n\nHorizontal line to.\n\nsource\n\n\nPathFT.V\n\n PathFT.V (y)\n\nVertical line to.\n\nsource\n\n\nPathFT.Z\n\n PathFT.Z ()\n\nClose path.\n\nsource\n\n\nPathFT.C\n\n PathFT.C (x1, y1, x2, y2, x, y)\n\nCubic Bézier curve.\n\nsource\n\n\nPathFT.S\n\n PathFT.S (x2, y2, x, y)\n\nSmooth cubic Bézier curve.\n\nsource\n\n\nPathFT.Q\n\n PathFT.Q (x1, y1, x, y)\n\nQuadratic Bézier curve.\n\nsource\n\n\nPathFT.T\n\n PathFT.T (x, y)\n\nSmooth quadratic Bézier curve.\n\nsource\n\n\nPathFT.A\n\n PathFT.A (rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, x, y)\n\nElliptical Arc.",
+ "crumbs": [
+ "Home",
+ "Source",
+ "SVG"
+ ]
+ },
+ {
+ "objectID": "api/svg.html#htmx-helpers",
+ "href": "api/svg.html#htmx-helpers",
+ "title": "SVG",
+ "section": "HTMX helpers",
+ "text": "HTMX helpers\n\nsource\n\nSvgOob\n\n SvgOob (*args, **kwargs)\n\nWraps an SVG shape as required for an HTMX OOB swap\nWhen returning an SVG shape out-of-band (OOB) in HTMX, you need to wrap it with SvgOob to have it appear correctly. (SvgOob is just a shortcut for Template(Svg(...)), which is the trick that makes SVG OOB swaps work.)\n\nsource\n\n\nSvgInb\n\n SvgInb (*args, **kwargs)\n\nWraps an SVG shape as required for an HTMX inband swap\nWhen returning an SVG shape in-band in HTMX, either have the calling element include hx_select='svg>*', or **svg_inb (which are two ways of saying the same thing), or wrap the response with SvgInb to have it appear correctly. (SvgInb is just a shortcut for the tuple (Svg(...), HtmxResponseHeaders(hx_reselect='svg>*')), which is the trick that makes SVG in-band swaps work.)",
+ "crumbs": [
+ "Home",
+ "Source",
+ "SVG"
+ ]
+ },
+ {
+ "objectID": "api/pico.html",
+ "href": "api/pico.html",
+ "title": "Pico.css components",
+ "section": "",
+ "text": "picocondlink is the class-conditional css link tag, and picolink is the regular tag.\n\nshow(picocondlink)\n\n\n\n\n\n\n\nsource\n\nset_pico_cls\n\n set_pico_cls ()\n\nRun this to make jupyter outputs styled with pico:\n\nset_pico_cls()\n\n\n\n\n\nsource\n\n\nCard\n\n Card (*c, header=None, footer=None, target_id=None, hx_vals=None,\n id=None, cls=None, title=None, style=None, accesskey=None,\n contenteditable=None, dir=None, draggable=None, enterkeyhint=None,\n hidden=None, inert=None, inputmode=None, lang=None, popover=None,\n spellcheck=None, tabindex=None, translate=None, hx_get=None,\n hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,\n hx_trigger=None, hx_target=None, hx_swap=None, hx_swap_oob=None,\n hx_include=None, hx_select=None, hx_select_oob=None,\n hx_indicator=None, hx_push_url=None, hx_confirm=None,\n hx_disable=None, hx_replace_url=None, hx_disabled_elt=None,\n hx_ext=None, hx_headers=None, hx_history=None, hx_history_elt=None,\n hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None, **kwargs)\n\nA PicoCSS Card, implemented as an Article with optional Header and Footer\n\nshow(Card('body', header=P('head'), footer=P('foot')))\n\n\n head\n\nbody\n foot\n\n\n\n\n\nsource\n\n\nGroup\n\n Group (*c, target_id=None, hx_vals=None, id=None, cls=None, title=None,\n style=None, accesskey=None, contenteditable=None, dir=None,\n draggable=None, enterkeyhint=None, hidden=None, inert=None,\n inputmode=None, lang=None, popover=None, spellcheck=None,\n tabindex=None, translate=None, hx_get=None, hx_post=None,\n hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,\n hx_target=None, hx_swap=None, hx_swap_oob=None, hx_include=None,\n hx_select=None, hx_select_oob=None, hx_indicator=None,\n hx_push_url=None, hx_confirm=None, hx_disable=None,\n hx_replace_url=None, hx_disabled_elt=None, hx_ext=None,\n hx_headers=None, hx_history=None, hx_history_elt=None,\n hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None, **kwargs)\n\nA PicoCSS Group, implemented as a Fieldset with role ‘group’\n\nshow(Group(Input(), Button(\"Save\")))\n\n\n \n Save\n\n\n\n\nsource\n\n\nSearch\n\n Search (*c, target_id=None, hx_vals=None, id=None, cls=None, title=None,\n style=None, accesskey=None, contenteditable=None, dir=None,\n draggable=None, enterkeyhint=None, hidden=None, inert=None,\n inputmode=None, lang=None, popover=None, spellcheck=None,\n tabindex=None, translate=None, hx_get=None, hx_post=None,\n hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,\n hx_target=None, hx_swap=None, hx_swap_oob=None, hx_include=None,\n hx_select=None, hx_select_oob=None, hx_indicator=None,\n hx_push_url=None, hx_confirm=None, hx_disable=None,\n hx_replace_url=None, hx_disabled_elt=None, hx_ext=None,\n hx_headers=None, hx_history=None, hx_history_elt=None,\n hx_inherit=None, hx_params=None, hx_preserve=None,\n hx_prompt=None, hx_request=None, hx_sync=None, hx_validate=None,\n **kwargs)\n\nA PicoCSS Search, implemented as a Form with role ‘search’\n\nshow(Search(Input(type=\"search\"), Button(\"Search\")))\n\n\n \n Search\n\n\n\n\nsource\n\n\nGrid\n\n Grid (*c, cls='grid', target_id=None, hx_vals=None, id=None, title=None,\n style=None, accesskey=None, contenteditable=None, dir=None,\n draggable=None, enterkeyhint=None, hidden=None, inert=None,\n inputmode=None, lang=None, popover=None, spellcheck=None,\n tabindex=None, translate=None, hx_get=None, hx_post=None,\n hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,\n hx_target=None, hx_swap=None, hx_swap_oob=None, hx_include=None,\n hx_select=None, hx_select_oob=None, hx_indicator=None,\n hx_push_url=None, hx_confirm=None, hx_disable=None,\n hx_replace_url=None, hx_disabled_elt=None, hx_ext=None,\n hx_headers=None, hx_history=None, hx_history_elt=None,\n hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None, **kwargs)\n\nA PicoCSS Grid, implemented as child Divs in a Div with class ‘grid’\n\ncolors = [Input(type=\"color\", value=o) for o in ('#e66465', '#53d2c5', '#f6b73c')]\nshow(Grid(*colors))\n\n\n \n\n \n\n \n\n\n\n\n\nsource\n\n\nDialogX\n\n DialogX (*c, open=None, header=None, footer=None, id=None,\n target_id=None, hx_vals=None, cls=None, title=None, style=None,\n accesskey=None, contenteditable=None, dir=None, draggable=None,\n enterkeyhint=None, hidden=None, inert=None, inputmode=None,\n lang=None, popover=None, spellcheck=None, tabindex=None,\n translate=None, hx_get=None, hx_post=None, hx_put=None,\n hx_delete=None, hx_patch=None, hx_trigger=None, hx_target=None,\n hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None, **kwargs)\n\nA PicoCSS Dialog, with children inside a Card\n\nhdr = Div(Button(aria_label=\"Close\", rel=\"prev\"), P('confirm'))\nftr = Div(Button('Cancel', cls=\"secondary\"), Button('Confirm'))\nd = DialogX('thank you!', header=hdr, footer=ftr, open=None, id='dlgtest')\n# use js or htmx to display modal\n\n\nsource\n\n\nContainer\n\n Container (*args, target_id=None, hx_vals=None, id=None, cls=None,\n title=None, style=None, accesskey=None, contenteditable=None,\n dir=None, draggable=None, enterkeyhint=None, hidden=None,\n inert=None, inputmode=None, lang=None, popover=None,\n spellcheck=None, tabindex=None, translate=None, hx_get=None,\n hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,\n hx_trigger=None, hx_target=None, hx_swap=None,\n hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None, **kwargs)\n\nA PicoCSS Container, implemented as a Main with class ‘container’\n\nsource\n\n\nPicoBusy\n\n PicoBusy ()",
+ "crumbs": [
+ "Home",
+ "Source",
+ "Pico.css components"
+ ]
+ },
+ {
+ "objectID": "api/xtend.html",
+ "href": "api/xtend.html",
+ "title": "Component extensions",
+ "section": "",
+ "text": "from pprint import pprint\nsource",
+ "crumbs": [
+ "Home",
+ "Source",
+ "Component extensions"
+ ]
+ },
+ {
+ "objectID": "api/xtend.html#forms",
+ "href": "api/xtend.html#forms",
+ "title": "Component extensions",
+ "section": "Forms",
+ "text": "Forms\n\nsource\n\nForm\n\n Form (*c, enctype='multipart/form-data', target_id=None, hx_vals=None,\n id=None, cls=None, title=None, style=None, accesskey=None,\n contenteditable=None, dir=None, draggable=None, enterkeyhint=None,\n hidden=None, inert=None, inputmode=None, lang=None, popover=None,\n spellcheck=None, tabindex=None, translate=None, hx_get=None,\n hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,\n hx_trigger=None, hx_target=None, hx_swap=None, hx_swap_oob=None,\n hx_include=None, hx_select=None, hx_select_oob=None,\n hx_indicator=None, hx_push_url=None, hx_confirm=None,\n hx_disable=None, hx_replace_url=None, hx_disabled_elt=None,\n hx_ext=None, hx_headers=None, hx_history=None, hx_history_elt=None,\n hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None, **kwargs)\n\nA Form tag; identical to plain ft_hx version except default enctype='multipart/form-data'\n\nsource\n\n\nHidden\n\n Hidden (value:Any='', id:Any=None, target_id=None, hx_vals=None,\n cls=None, title=None, style=None, accesskey=None,\n contenteditable=None, dir=None, draggable=None,\n enterkeyhint=None, hidden=None, inert=None, inputmode=None,\n lang=None, popover=None, spellcheck=None, tabindex=None,\n translate=None, hx_get=None, hx_post=None, hx_put=None,\n hx_delete=None, hx_patch=None, hx_trigger=None, hx_target=None,\n hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None, **kwargs)\n\nAn Input of type ‘hidden’\n\nsource\n\n\nCheckboxX\n\n CheckboxX (checked:bool=False, label=None, value='1', id=None, name=None,\n target_id=None, hx_vals=None, cls=None, title=None,\n style=None, accesskey=None, contenteditable=None, dir=None,\n draggable=None, enterkeyhint=None, hidden=None, inert=None,\n inputmode=None, lang=None, popover=None, spellcheck=None,\n tabindex=None, translate=None, hx_get=None, hx_post=None,\n hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,\n hx_target=None, hx_swap=None, hx_swap_oob=None,\n hx_include=None, hx_select=None, hx_select_oob=None,\n hx_indicator=None, hx_push_url=None, hx_confirm=None,\n hx_disable=None, hx_replace_url=None, hx_disabled_elt=None,\n hx_ext=None, hx_headers=None, hx_history=None,\n hx_history_elt=None, hx_inherit=None, hx_params=None,\n hx_preserve=None, hx_prompt=None, hx_request=None,\n hx_sync=None, hx_validate=None, **kwargs)\n\nA Checkbox optionally inside a Label, preceded by a Hidden with matching name\n\nshow(CheckboxX(True, 'Check me out!'))\n\n\n \nCheck me out!\n\n\n\nsource\n\n\nScript\n\n Script (code:str='', id=None, cls=None, title=None, style=None,\n attrmap=None, valmap=None, ft_cls=None, auto_id=None, **kwargs)\n\nA Script tag that doesn’t escape its code\n\nsource\n\n\nStyle\n\n Style (*c, id=None, cls=None, title=None, style=None, attrmap=None,\n valmap=None, ft_cls=None, auto_id=None, **kwargs)\n\nA Style tag that doesn’t escape its code",
+ "crumbs": [
+ "Home",
+ "Source",
+ "Component extensions"
+ ]
+ },
+ {
+ "objectID": "api/xtend.html#style-and-script-templates",
+ "href": "api/xtend.html#style-and-script-templates",
+ "title": "Component extensions",
+ "section": "Style and script templates",
+ "text": "Style and script templates\n\nsource\n\ndouble_braces\n\n double_braces (s)\n\nConvert single braces to double braces if next to special chars or newline\n\nsource\n\n\nundouble_braces\n\n undouble_braces (s)\n\nConvert double braces to single braces if next to special chars or newline\n\nsource\n\n\nloose_format\n\n loose_format (s, **kw)\n\nString format s using kw, without being strict about braces outside of template params\n\nsource\n\n\nScriptX\n\n ScriptX (fname, src=None, nomodule=None, type=None, _async=None,\n defer=None, charset=None, crossorigin=None, integrity=None,\n **kw)\n\nA script element with contents read from fname\n\nsource\n\n\nreplace_css_vars\n\n replace_css_vars (css, pre='tpl', **kwargs)\n\nReplace var(--) CSS variables with kwargs if name prefix matches pre\n\nsource\n\n\nStyleX\n\n StyleX (fname, **kw)\n\nA style element with contents read from fname and variables replaced from kw\n\nsource\n\n\nNbsp\n\n Nbsp ()\n\nA non-breaking space",
+ "crumbs": [
+ "Home",
+ "Source",
+ "Component extensions"
+ ]
+ },
+ {
+ "objectID": "api/xtend.html#surreal-and-js",
+ "href": "api/xtend.html#surreal-and-js",
+ "title": "Component extensions",
+ "section": "Surreal and JS",
+ "text": "Surreal and JS\n\nsource\n\nSurreal\n\n Surreal (code:str)\n\nWrap code in domReadyExecute and set m=me() and p=me('-')\n\nsource\n\n\nOn\n\n On (code:str, event:str='click', sel:str='', me=True)\n\nAn async surreal.js script block event handler for event on selector sel,p, making available parent p, event ev, and target e\n\nsource\n\n\nPrev\n\n Prev (code:str, event:str='click')\n\nAn async surreal.js script block event handler for event on previous sibling, with same vars as On\n\nsource\n\n\nNow\n\n Now (code:str, sel:str='')\n\nAn async surreal.js script block on selector me(sel)\n\nsource\n\n\nAnyNow\n\n AnyNow (sel:str, code:str)\n\nAn async surreal.js script block on selector any(sel)\n\nsource\n\n\nrun_js\n\n run_js (js, id=None, **kw)\n\nRun js script, auto-generating id based on name of caller if needed, and js-escaping any kw params\n\nsource\n\n\nHtmxOn\n\n HtmxOn (eventname:str, code:str)\n\n\nsource\n\n\njsd\n\n jsd (org, repo, root, path, prov='gh', typ='script', ver=None, esm=False,\n **kwargs)\n\njsdelivr Script or CSS Link tag, or URL",
+ "crumbs": [
+ "Home",
+ "Source",
+ "Component extensions"
+ ]
+ },
+ {
+ "objectID": "api/xtend.html#other-helpers",
+ "href": "api/xtend.html#other-helpers",
+ "title": "Component extensions",
+ "section": "Other helpers",
+ "text": "Other helpers\n\nsource\n\nTitled\n\n Titled (title:str='FastHTML app', *args, cls='container', target_id=None,\n hx_vals=None, id=None, style=None, accesskey=None,\n contenteditable=None, dir=None, draggable=None,\n enterkeyhint=None, hidden=None, inert=None, inputmode=None,\n lang=None, popover=None, spellcheck=None, tabindex=None,\n translate=None, hx_get=None, hx_post=None, hx_put=None,\n hx_delete=None, hx_patch=None, hx_trigger=None, hx_target=None,\n hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None, **kwargs)\n\nAn HTML partial containing a Title, and H1, and any provided children\n\nsource\n\n\nSocials\n\n Socials (title, site_name, description, image, url=None, w=1200, h=630,\n twitter_site=None, creator=None, card='summary')\n\nOG and Twitter social card headers\n\nsource\n\n\nFavicon\n\n Favicon (light_icon, dark_icon)\n\nLight and dark favicon headers\n\nsource\n\n\nclear\n\n clear (id)",
+ "crumbs": [
+ "Home",
+ "Source",
+ "Component extensions"
+ ]
+ },
+ {
+ "objectID": "explains/routes.html",
+ "href": "explains/routes.html",
+ "title": "Routes",
+ "section": "",
+ "text": "Behaviour in FastHTML apps is defined by routes. The syntax is largely the same as the wonderful FastAPI (which is what you should be using instead of this if you’re creating a JSON service. FastHTML is mainly for making HTML web apps, not APIs).\nNote that you need to include the types of your parameters, so that FastHTML knows what to pass to your function. Here, we’re just expecting a string:\nfrom fasthtml.common import *\napp = FastHTML()\n\n@app.get('/user/{nm}')\ndef get_nm(nm:str): return f\"Good day to you, {nm}!\"\nNormally you’d save this into a file such as main.py, and then run it in uvicorn using:\nHowever, for testing, we can use Starlette’s TestClient to try it out:\nfrom starlette.testclient import TestClient\nclient = TestClient(app)\nr = client.get('/user/Jeremy')\nr\n\n<Response [200 OK]>\nTestClient uses httpx behind the scenes, so it returns a httpx.Response, which has a text attribute with our response body:\nr.text\n\n'Good day to you, Jeremy!'\nIn the previous example, the function name (get_nm) didn’t actually matter – we could have just called it _, for instance, since we never actually call it directly. It’s just called through HTTP. In fact, we often do call our functions _ when using this style of route, since that’s one less thing we have to worry about, naming.\nAn alternative approach to creating a route is to use app.route instead, in which case, you make the function name the HTTP method you want. Since this is such a common pattern, you might like to give a shorter name to app.route – we normally use rt:\nrt = app.route\n\n@rt('/')\ndef post(): return \"Going postal!\"\n\nclient.post('/').text\n\n'Going postal!'",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "Routes"
+ ]
+ },
+ {
+ "objectID": "explains/routes.html#combining-routes",
+ "href": "explains/routes.html#combining-routes",
+ "title": "Routes",
+ "section": "Combining Routes",
+ "text": "Combining Routes\nSometimes a FastHTML project can grow so weildy that putting all the routes into main.py becomes unweildy. Or, we install a FastHTML- or Starlette-based package that requires us to add routes.\nFirst let’s create a books.py module, that represents all the user-related views:\n\n# books.py\nbooks_app, rt = fast_app()\n\nbooks = ['A Guide to FastHTML', 'FastHTML Cookbook', 'FastHTML in 24 Hours']\n\n@rt(\"/\", name=\"list\")\ndef get():\n return Titled(\"Books\", *[P(book) for book in books])\n\nLet’s mount it in our main module:\nfrom books import app as books_app\n\n1app, rt = fast_app(routes=[Mount(\"/books\", books_app, name=\"books\")])\n\n@rt(\"/\")\ndef get():\n return Titled(\"Dashboard\",\n2 P(A(href=\"/books\")(\"Books\")),\n Hr(),\n3 P(A(link=uri(\"books:list\"))(\"Books\")),\n )\n\nserve()\n\n1\n\nWe use starlette.Mount to add the route to our routes list. We provide the name of books to make discovery and management of the links easier. More on that in items 2 and 3 of this annotations list\n\n2\n\nThis example link to the books list view is hand-crafted. Obvious in purpose, it makes changing link patterns in the future harder\n\n3\n\nThis example link uses the named URL route for the books. The advantage of this approach is it makes management of large numbers of link items easier.",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "Routes"
+ ]
+ },
+ {
+ "objectID": "explains/faq.html",
+ "href": "explains/faq.html",
+ "title": "FAQ",
+ "section": "",
+ "text": "Many editors, including Visual Studio Code, use PyLance to provide error checking for Python. However, PyLance’s error checking is just a guess – it can’t actually know whether your code is correct or not. PyLance particularly struggles with FastHTML’s syntax, which leads to it often reporting false error messages in FastHTML projects.\nTo avoid these misleading error messages, it’s best to disable some PyLance error checking in your FastHTML projects. Here’s how to do it in Visual Studio Code (the same approach should also work in other editors based on vscode, such as Cursor and GitHub Codespaces):\n\nOpen your FastHTML project\nPress Ctrl+Shift+P (or Cmd+Shift+P on Mac) to open the Command Palette\nType “Preferences: Open Workspace Settings (JSON)” and select it\nIn the JSON file that opens, add the following lines:\n\n{\n \"python.analysis.diagnosticSeverityOverrides\": {\n \"reportGeneralTypeIssues\": \"none\",\n \"reportOptionalMemberAccess\": \"none\",\n \"reportWildcardImportFromLibrary\": \"none\",\n \"reportRedeclaration\": \"none\",\n \"reportAttributeAccessIssue\": \"none\",\n \"reportInvalidTypeForm\": \"none\",\n \"reportAssignmentType\": \"none\",\n }\n}\n\nSave the file\n\nEven with PyLance diagnostics turned off, your FastHTML code will still run correctly. If you’re still seeing some false errors from PyLance, you can disable it entirely by adding this to your settings:\n{\n \"python.analysis.ignore\": [ \"*\" ]\n}",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "FAQ"
+ ]
+ },
+ {
+ "objectID": "explains/faq.html#why-does-my-editor-say-that-i-have-errors-in-my-fasthtml-code",
+ "href": "explains/faq.html#why-does-my-editor-say-that-i-have-errors-in-my-fasthtml-code",
+ "title": "FAQ",
+ "section": "",
+ "text": "Many editors, including Visual Studio Code, use PyLance to provide error checking for Python. However, PyLance’s error checking is just a guess – it can’t actually know whether your code is correct or not. PyLance particularly struggles with FastHTML’s syntax, which leads to it often reporting false error messages in FastHTML projects.\nTo avoid these misleading error messages, it’s best to disable some PyLance error checking in your FastHTML projects. Here’s how to do it in Visual Studio Code (the same approach should also work in other editors based on vscode, such as Cursor and GitHub Codespaces):\n\nOpen your FastHTML project\nPress Ctrl+Shift+P (or Cmd+Shift+P on Mac) to open the Command Palette\nType “Preferences: Open Workspace Settings (JSON)” and select it\nIn the JSON file that opens, add the following lines:\n\n{\n \"python.analysis.diagnosticSeverityOverrides\": {\n \"reportGeneralTypeIssues\": \"none\",\n \"reportOptionalMemberAccess\": \"none\",\n \"reportWildcardImportFromLibrary\": \"none\",\n \"reportRedeclaration\": \"none\",\n \"reportAttributeAccessIssue\": \"none\",\n \"reportInvalidTypeForm\": \"none\",\n \"reportAssignmentType\": \"none\",\n }\n}\n\nSave the file\n\nEven with PyLance diagnostics turned off, your FastHTML code will still run correctly. If you’re still seeing some false errors from PyLance, you can disable it entirely by adding this to your settings:\n{\n \"python.analysis.ignore\": [ \"*\" ]\n}",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "FAQ"
+ ]
+ },
+ {
+ "objectID": "explains/faq.html#why-the-distinctive-coding-style",
+ "href": "explains/faq.html#why-the-distinctive-coding-style",
+ "title": "FAQ",
+ "section": "Why the distinctive coding style?",
+ "text": "Why the distinctive coding style?\nFastHTML coding style is the fastai coding style.\nIf you are coming from a data science background the fastai coding style may already be your preferred style.\nIf you are coming from a PEP-8 background where the use of ruff is encouraged, there is a learning curve. However, once you get used to the fastai coding style you may discover yourself appreciating the concise nature of this style. It also encourages using more functional programming tooling, which is both productive and fun. Having said that, it’s entirely optional!",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "FAQ"
+ ]
+ },
+ {
+ "objectID": "explains/faq.html#why-not-jsx",
+ "href": "explains/faq.html#why-not-jsx",
+ "title": "FAQ",
+ "section": "Why not JSX?",
+ "text": "Why not JSX?\nMany have asked! We think there’s no benefit… Python’s positional and kw args precisely 1:1 map already to html/xml children and attrs, so there’s no need for a new syntax.\nWe wrote some more thoughts on Why Python HTML components over Jinja2, Mako, or JSX here.",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "FAQ"
+ ]
+ },
+ {
+ "objectID": "explains/faq.html#why-use-import",
+ "href": "explains/faq.html#why-use-import",
+ "title": "FAQ",
+ "section": "Why use import *",
+ "text": "Why use import *\nFirst, through the use of the __all__ attribute in our Python modules we control what actually gets imported. So there’s no risk of namespace pollution.\nSecond, our style lends itself to working in rather compact Jupyter notebooks and small Python modules. Hence we know about the source code whose libraries we import * from. This terseness means we can develop faster. We’re a small team, and any edge we can gain is important to us.\nThird, for external libraries, be it core Python, SQLAlchemy, or other things we do tend to use explicit imports. In part to avoid namespace collisions, and also as reference to know where things are coming from.\nWe’ll finish by saying a lot of our users employ explicit imports. If that’s the path you want to take, we encourage the use of from fasthtml import common as fh. The acronym of fh makes it easy to recognize that a symbol is from the FastHTML library.",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "FAQ"
+ ]
+ },
+ {
+ "objectID": "explains/faq.html#can-fasthtml-be-used-for-dashboards",
+ "href": "explains/faq.html#can-fasthtml-be-used-for-dashboards",
+ "title": "FAQ",
+ "section": "Can FastHTML be used for dashboards?",
+ "text": "Can FastHTML be used for dashboards?\nYes it can. In fact, it excels at building dashboards. In addition to being great for building static dashboards, because of its foundation in ASGI and tech stack, FastHTML natively supports Websockets. That means using FastHTML we can create dashboards that autoupdate.",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "FAQ"
+ ]
+ },
+ {
+ "objectID": "explains/faq.html#why-is-fasthtml-developed-using-notebooks",
+ "href": "explains/faq.html#why-is-fasthtml-developed-using-notebooks",
+ "title": "FAQ",
+ "section": "Why is FastHTML developed using notebooks?",
+ "text": "Why is FastHTML developed using notebooks?\nSome people are under the impression that writing software in notebooks is bad.\nWatch this video. We’ve used Jupyter notebooks exported via nbdev to write a wide range of “very serious” software projects over the last three years. This includes deep learning libraries, API clients, Python language extensions, terminal user interfaces, web frameworks, and more!\nnbdev is a Jupyter-powered tool for writing software. Traditional programming environments throw away the result of your exploration in REPLs or notebooks. nbdev makes exploration an integral part of your workflow, all while promoting software engineering best practices.",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "FAQ"
+ ]
+ },
+ {
+ "objectID": "explains/faq.html#why-not-pyproject.toml-for-packaging",
+ "href": "explains/faq.html#why-not-pyproject.toml-for-packaging",
+ "title": "FAQ",
+ "section": "Why not pyproject.toml for packaging?",
+ "text": "Why not pyproject.toml for packaging?\nFastHTML uses a setup.py module instead of a pyproject.toml file to configure itself for installation. The reason for this is pyproject.toml is not compatible with nbdev, which is what is used to write and build FastHTML.\nThe nbdev project spent around a year trying to move to pyproject.toml but there was insufficient functionality in the toml-based approach to complete the transition.",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "FAQ"
+ ]
+ },
+ {
+ "objectID": "explains/explaining_xt_components.html",
+ "href": "explains/explaining_xt_components.html",
+ "title": "FT Components",
+ "section": "",
+ "text": "FT, or ‘FastTags’, are the display components of FastHTML. In fact, the word “components” in the context of FastHTML is often synonymous with FT.\nFor example, when we look at a FastHTML app, in particular the views, as well as various functions and other objects, we see something like the code snippet below. It’s the return statement that we want to pay attention to:\nfrom fasthtml.common import *\n\ndef example():\n # The code below is a set of ft components\n return Div(\n H1(\"FastHTML APP\"),\n P(\"Let's do this\"),\n cls=\"go\"\n )\nLet’s go ahead and call our function and print the result:\nexample()\n\n<div class=\"go\">\n <h1>FastHTML APP</h1>\n <p>Let's do this</p>\n</div>\nAs you can see, when returned to the user from a Python callable, like a function, the ft components are transformed into their string representations of XML or XML-like content such as HTML. More concisely, ft turns Python objects into HTML.\nNow that we know what ft components look and behave like we can begin to understand them. At their most fundamental level, ft components:",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "**FT** Components"
+ ]
+ },
+ {
+ "objectID": "explains/explaining_xt_components.html#how-fasthtml-names-ft-components",
+ "href": "explains/explaining_xt_components.html#how-fasthtml-names-ft-components",
+ "title": "FT Components",
+ "section": "How FastHTML names ft components",
+ "text": "How FastHTML names ft components\nWhen it comes to naming ft components, FastHTML appears to break from PEP8. Specifically, PEP8 specifies that when naming variables, functions and instantiated classes we use the snake_case_pattern. That is to say, lowercase with words separated by underscores. However, FastHTML uses PascalCase for ft components.\nThere’s a couple of reasons for this:\n\nft components can be made from any callable type, so adhering to any one pattern doesn’t make much sense\nIt makes for easier reading of FastHTML code, as anything that is PascalCase is probably an ft component",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "**FT** Components"
+ ]
+ },
+ {
+ "objectID": "explains/explaining_xt_components.html#default-ft-components",
+ "href": "explains/explaining_xt_components.html#default-ft-components",
+ "title": "FT Components",
+ "section": "Default FT components",
+ "text": "Default FT components\nFastHTML has over 150 FT components designed to accelerate web development. Most of these mirror HTML tags such as <div>, <p>, <a>, <title>, and more. However, there are some extra tags added, including:\n\nTitled, a combination of the Title() and H1() tags\nSocials, renders popular social media tags",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "**FT** Components"
+ ]
+ },
+ {
+ "objectID": "explains/explaining_xt_components.html#the-fasthtml.ft-namespace",
+ "href": "explains/explaining_xt_components.html#the-fasthtml.ft-namespace",
+ "title": "FT Components",
+ "section": "The fasthtml.ft Namespace",
+ "text": "The fasthtml.ft Namespace\nSome people prefer to write code using namespaces while adhering to PEP8. If that’s a preference, projects can be coded using the fasthtml.ft namespace.\n\nfrom fasthtml import ft\n\nft.Ul(\n ft.Li(\"one\"),\n ft.Li(\"two\"),\n ft.Li(\"three\")\n)\n\n<ul>\n <li>one</li>\n <li>two</li>\n <li>three</li>\n</ul>",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "**FT** Components"
+ ]
+ },
+ {
+ "objectID": "explains/explaining_xt_components.html#attributes",
+ "href": "explains/explaining_xt_components.html#attributes",
+ "title": "FT Components",
+ "section": "Attributes",
+ "text": "Attributes\nThis example demonstrates many important things to know about how ft components handle attributes.\n#| echo: False\n1Label(\n \"Choose an option\", \n Select(\n2 Option(\"one\", value=\"1\", selected=True),\n3 Option(\"two\", value=\"2\", selected=False),\n4 Option(\"three\", value=3),\n5 cls=\"selector\",\n6 _id=\"counter\",\n7 **{'@click':\"alert('Clicked');\"},\n ),\n8 _for=\"counter\",\n)\n\n1\n\nLine 2 demonstrates that FastHTML appreciates Labels surrounding their fields.\n\n2\n\nOn line 5, we can see that attributes set to the boolean value of True are rendered with just the name of the attribute.\n\n3\n\nOn line 6, we demonstrate that attributes set to the boolean value of False do not appear in the rendered output.\n\n4\n\nLine 7 is an example of how integers and other non-string values in the rendered output are converted to strings.\n\n5\n\nLine 8 is where we set the HTML class using the cls argument. We use cls here as class is a reserved word in Python. During the rendering process this will be converted to the word “class”.\n\n6\n\nLine 9 demonstrates that any named argument passed into an ft component will have the leading underscore stripped away before rendering. Useful for handling reserved words in Python.\n\n7\n\nOn line 10 we have an attribute name that cannot be represented as a python variable. In cases like these, we can use an unpacked dict to represent these values.\n\n8\n\nThe use of _for on line 12 is another demonstration of an argument having the leading underscore stripped during render. We can also use fr as that will be expanded to for.\n\n\nThis renders the following HTML snippet:\n\nLabel(\n \"Choose an option\", \n Select(\n Option(\"one\", value=\"1\", selected=True),\n Option(\"two\", value=\"2\", selected=False),\n Option(\"three\", value=3), # <4>,\n cls=\"selector\",\n _id=\"counter\",\n **{'@click':\"alert('Clicked');\"},\n ),\n _for=\"counter\",\n)\n\n<label for=\"counter\">\nChoose an option\n <select id=\"counter\" @click=\"alert('Clicked');\" class=\"selector\" name=\"counter\">\n <option value=\"1\" selected>one</option>\n <option value=\"2\" >two</option>\n <option value=\"3\">three</option>\n </select>\n</label>",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "**FT** Components"
+ ]
+ },
+ {
+ "objectID": "explains/explaining_xt_components.html#defining-new-ft-components",
+ "href": "explains/explaining_xt_components.html#defining-new-ft-components",
+ "title": "FT Components",
+ "section": "Defining new ft components",
+ "text": "Defining new ft components\nIt is possible and sometimes useful to create your own ft components that generate non-standard tags that are not in the FastHTML library. FastHTML supports created and defining those new tags flexibly.\nFor more information, see the Defining new ft components reference page.",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "**FT** Components"
+ ]
+ },
+ {
+ "objectID": "explains/explaining_xt_components.html#ft-components-and-type-hints",
+ "href": "explains/explaining_xt_components.html#ft-components-and-type-hints",
+ "title": "FT Components",
+ "section": "FT components and type hints",
+ "text": "FT components and type hints\nIf you use type hints, we strongly suggest that FT components be treated as the Any type.\nThe reason is that FastHTML leverages python’s dynamic features to a great degree. Especially when it comes to FT components, which can evaluate out to be FT|str|None|tuple as well as anything that supports the __ft__, __html__, and __str__ method. That’s enough of the Python stack that assigning anything but Any to be the FT type will prove an exercise in frustation.",
+ "crumbs": [
+ "Home",
+ "Explanations",
+ "**FT** Components"
+ ]
+ },
+ {
+ "objectID": "ref/handlers.html",
+ "href": "ref/handlers.html",
+ "title": "Handling handlers",
+ "section": "",
+ "text": "from fasthtml.common import *\nfrom collections import namedtuple\nfrom typing import TypedDict\nfrom datetime import datetime\nimport json,time\napp = FastHTML()\nThe FastHTML class is the main application class for FastHTML apps.\nrt = app.route\napp.route is used to register route handlers. It is a decorator, which means we place it before a function that is used as a handler. Because it’s used frequently in most FastHTML applications, we often alias it as rt, as we do here.",
+ "crumbs": [
+ "Home",
+ "Reference",
+ "Handling handlers"
+ ]
+ },
+ {
+ "objectID": "ref/handlers.html#basic-route-handling",
+ "href": "ref/handlers.html#basic-route-handling",
+ "title": "Handling handlers",
+ "section": "Basic Route Handling",
+ "text": "Basic Route Handling\n\n@rt(\"/hi\")\ndef get(): return 'Hi there'\n\nHandler functions can return strings directly. These strings are sent as the response body to the client.\n\ncli = Client(app)\n\nClient is a test client for FastHTML applications. It allows you to simulate requests to your app without running a server.\n\ncli.get('/hi').text\n\n'Hi there'\n\n\nThe get method on a Client instance simulates GET requests to the app. It returns a response object that has a .text attribute, which you can use to access the body of the response. It calls httpx.get internally – all httpx HTTP verbs are supported.\n\n@rt(\"/hi\")\ndef post(): return 'Postal'\ncli.post('/hi').text\n\n'Postal'\n\n\nHandler functions can be defined for different HTTP methods on the same route. Here, we define a post handler for the /hi route. The Client instance can simulate different HTTP methods, including POST requests.",
+ "crumbs": [
+ "Home",
+ "Reference",
+ "Handling handlers"
+ ]
+ },
+ {
+ "objectID": "ref/handlers.html#request-and-response-objects",
+ "href": "ref/handlers.html#request-and-response-objects",
+ "title": "Handling handlers",
+ "section": "Request and Response Objects",
+ "text": "Request and Response Objects\n\n@app.get(\"/hostie\")\ndef show_host(req): return req.headers['host']\ncli.get('/hostie').text\n\n'testserver'\n\n\nHandler functions can accept a req (or request) parameter, which represents the incoming request. This object contains information about the request, including headers. In this example, we return the host header from the request. The test client uses ‘testserver’ as the default host.\nIn this example, we use @app.get(\"/hostie\") instead of @rt(\"/hostie\"). The @app.get() decorator explicitly specifies the HTTP method (GET) for the route, while @rt() by default handles both GET and POST requests.\n\n@rt\ndef yoyo(): return 'a yoyo'\ncli.post('/yoyo').text\n\n'a yoyo'\n\n\nIf the @rt decorator is used without arguments, it uses the function name as the route path. Here, the yoyo function becomes the handler for the /yoyo route. This handler responds to GET and POST methods, since a specific method wasn’t provided.\n\n@rt\ndef ft1(): return Html(Div('Text.'))\nprint(cli.get('/ft1').text)\n\n <html>\n <div>Text.</div>\n </html>\n\n\n\nHandler functions can return FT objects, which are automatically converted to HTML strings. The FT class can take other FT components as arguments, such as Div. This allows for easy composition of HTML elements in your responses.\n\n@app.get\ndef autopost(): return Html(Div('Text.', hx_post=yoyo.rt()))\nprint(cli.get('/autopost').text)\n\n <html>\n <div hx-post=\"/yoyo\">Text.</div>\n </html>\n\n\n\nThe rt decorator modifies the yoyo function by adding an rt() method. This method returns the route path associated with the handler. It’s a convenient way to reference the route of a handler function dynamically.\nIn the example, yoyo.rt() is used as the value for hx_post. This means when the div is clicked, it will trigger an HTMX POST request to the route of the yoyo handler. This approach allows for flexible, DRY code by avoiding hardcoded route strings and automatically updating if the route changes.\nThis pattern is particularly useful in larger applications where routes might change, or when building reusable components that need to reference their own routes dynamically.\n\n@app.get\ndef autoget(): return Html(Body(Div('Text.', cls='px-2', hx_post=show_host.rt(a='b'))))\nprint(cli.get('/autoget').text)\n\n <html>\n <body>\n <div hx-post=\"/hostie?a=b\" class=\"px-2\">Text.</div>\n </body>\n </html>\n\n\n\nThe rt() method of handler functions can also accept parameters. When called with parameters, it returns the route path with a query string appended. In this example, show_host.rt(a='b') generates the path /hostie?a=b.\nThe Body component is used here to demonstrate nesting of FT components. Div is nested inside Body, showcasing how you can create more complex HTML structures.\nThe cls parameter is used to add a CSS class to the Div. This translates to the class attribute in the rendered HTML. (class can’t be used as a parameter name directly in Python since it’s a reserved word.)\n\n@rt('/ft2')\ndef get(): return Title('Foo'),H1('bar')\nprint(cli.get('/ft2').text)\n\n <!doctype html>\n\n <html>\n <head>\n <title>Foo</title>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n <script src=\"https://unpkg.com/htmx.org@next/dist/htmx.min.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@main/fasthtml.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js\"></script>\n </head>\n <body>\n <h1>bar</h1>\n </body>\n </html>\n\n\n\nHandler functions can return multiple FT objects as a tuple. The first item is treated as the Title, and the rest are added to the Body. When the request is not an HTMX request, FastHTML automatically adds necessary HTML boilerplate, including default head content with required scripts.\nWhen using app.route (or rt), if the function name matches an HTTP verb (e.g., get, post, put, delete), that HTTP method is automatically used for the route. In this case, a path must be explicitly provided as an argument to the decorator.\n\nhxhdr = {'headers':{'hx-request':\"1\"}}\nprint(cli.get('/ft2', **hxhdr).text)\n\n <title>Foo</title>\n\n <h1>bar</h1>\n\n\n\nFor HTMX requests (indicated by the hx-request header), FastHTML returns only the specified components without the full HTML structure. This allows for efficient partial page updates in HTMX applications.\n\n@rt('/ft3')\ndef get(): return H1('bar')\nprint(cli.get('/ft3', **hxhdr).text)\n\n <h1>bar</h1>\n\n\n\nWhen a handler function returns a single FT object for an HTMX request, it’s rendered as a single HTML partial.\n\n@rt('/ft4')\ndef get(): return Html(Head(Title('hi')), Body(P('there')))\n\nprint(cli.get('/ft4').text)\n\n <html>\n <head>\n <title>hi</title>\n </head>\n <body>\n <p>there</p>\n </body>\n </html>\n\n\n\nHandler functions can return a complete Html structure, including Head and Body components. When a full HTML structure is returned, FastHTML doesn’t add any additional boilerplate. This gives you full control over the HTML output when needed.\n\n@rt\ndef index(): return \"welcome!\"\nprint(cli.get('/').text)\n\nwelcome!\n\n\nThe index function is a special handler in FastHTML. When defined without arguments to the @rt decorator, it automatically becomes the handler for the root path ('/'). This is a convenient way to define the main page or entry point of your application.",
+ "crumbs": [
+ "Home",
+ "Reference",
+ "Handling handlers"
+ ]
+ },
+ {
+ "objectID": "ref/handlers.html#path-and-query-parameters",
+ "href": "ref/handlers.html#path-and-query-parameters",
+ "title": "Handling handlers",
+ "section": "Path and Query Parameters",
+ "text": "Path and Query Parameters\n\n@rt('/user/{nm}', name='gday')\ndef get(nm:str=''): return f\"Good day to you, {nm}!\"\ncli.get('/user/Alexis').text\n\n'Good day to you, Alexis!'\n\n\nHandler functions can use path parameters, defined using curly braces in the route – this is implemented by Starlette directly, so all Starlette path parameters can be used. These parameters are passed as arguments to the function.\nThe name parameter in the decorator allows you to give the route a name, which can be used for URL generation.\nIn this example, {nm} in the route becomes the nm parameter in the function. The function uses this parameter to create a personalized greeting.\n\n@app.get\ndef autolink(): return Html(Div('Text.', link=uri('gday', nm='Alexis')))\nprint(cli.get('/autolink').text)\n\n <html>\n <div href=\"/user/Alexis\">Text.</div>\n </html>\n\n\n\nThe uri function is used to generate URLs for named routes. It takes the route name as its first argument, followed by any path or query parameters needed for that route.\nIn this example, uri('gday', nm='Alexis') generates the URL for the route named ‘gday’ (which we defined earlier as ‘/user/{nm}’), with ‘Alexis’ as the value for the ‘nm’ parameter.\nThe link parameter in FT components sets the href attribute of the rendered HTML element. By using uri(), we can dynamically generate correct URLs even if the underlying route structure changes.\nThis approach promotes maintainable code by centralizing route definitions and avoiding hardcoded URLs throughout the application.\n\n@rt('/link')\ndef get(req): return f\"{req.url_for('gday', nm='Alexis')}; {req.url_for('show_host')}\"\n\ncli.get('/link').text\n\n'http://testserver/user/Alexis; http://testserver/hostie'\n\n\nThe url_for method of the request object can be used to generate URLs for named routes. It takes the route name as its first argument, followed by any path parameters needed for that route.\nIn this example, req.url_for('gday', nm='Alexis') generates the full URL for the route named ‘gday’, including the scheme and host. Similarly, req.url_for('show_host') generates the URL for the ‘show_host’ route.\nThis method is particularly useful when you need to generate absolute URLs, such as for email links or API responses. It ensures that the correct host and scheme are included, even if the application is accessed through different domains or protocols.\n\napp.url_path_for('gday', nm='Jeremy')\n\n'/user/Jeremy'\n\n\nThe url_path_for method of the application can be used to generate URL paths for named routes. Unlike url_for, it returns only the path component of the URL, without the scheme or host.\nIn this example, app.url_path_for('gday', nm='Jeremy') generates the path ‘/user/Jeremy’ for the route named ‘gday’.\nThis method is useful when you need relative URLs or just the path component, such as for internal links or when constructing URLs in a host-agnostic manner.\n\n@rt('/oops')\ndef get(nope): return nope\nr = cli.get('/oops?nope=1')\nprint(r)\nr.text\n\n<Response [200 OK]>\n\n\n/Users/jhoward/Documents/GitHub/fasthtml/fasthtml/core.py:175: UserWarning: `nope has no type annotation and is not a recognised special name, so is ignored.\n if arg!='resp': warn(f\"`{arg} has no type annotation and is not a recognised special name, so is ignored.\")\n\n\n''\n\n\nHandler functions can include parameters, but they must be type-annotated or have special names (like req) to be recognized. In this example, the nope parameter is not annotated, so it’s ignored, resulting in a warning.\nWhen a parameter is ignored, it doesn’t receive the value from the query string. This can lead to unexpected behavior, as the function attempts to return nope, which is undefined.\nThe cli.get('/oops?nope=1') call succeeds with a 200 OK status because the handler doesn’t raise an exception, but it returns an empty response, rather than the intended value.\nTo fix this, you should either add a type annotation to the parameter (e.g., def get(nope: str):) or use a recognized special name like req.\n\n@rt('/html/{idx}')\ndef get(idx:int): return Body(H4(f'Next is {idx+1}.'))\nprint(cli.get('/html/1', **hxhdr).text)\n\n <body>\n <h4>Next is 2.</h4>\n </body>\n\n\n\nPath parameters can be type-annotated, and FastHTML will automatically convert them to the specified type if possible. In this example, idx is annotated as int, so it’s converted from the string in the URL to an integer.\n\nreg_re_param(\"imgext\", \"ico|gif|jpg|jpeg|webm\")\n\n@rt(r'/static/{path:path}{fn}.{ext:imgext}')\ndef get(fn:str, path:str, ext:str): return f\"Getting {fn}.{ext} from /{path}\"\n\nprint(cli.get('/static/foo/jph.ico').text)\n\nGetting jph.ico from /foo/\n\n\nThe reg_re_param function is used to register custom path parameter types using regular expressions. Here, we define a new path parameter type called “imgext” that matches common image file extensions.\nHandler functions can use complex path patterns with multiple parameters and custom types. In this example, the route pattern r'/static/{path:path}{fn}.{ext:imgext}' uses three path parameters:\n\npath: A Starlette built-in type that matches any path segments\nfn: The filename without extension\next: Our custom “imgext” type that matches specific image extensions\n\n\nModelName = str_enum('ModelName', \"alexnet\", \"resnet\", \"lenet\")\n\n@rt(\"/models/{nm}\")\ndef get(nm:ModelName): return nm\n\nprint(cli.get('/models/alexnet').text)\n\nalexnet\n\n\nWe define ModelName as an enum with three possible values: “alexnet”, “resnet”, and “lenet”. Handler functions can use these enum types as parameter annotations. In this example, the nm parameter is annotated with ModelName, which ensures that only valid model names are accepted.\nWhen a request is made with a valid model name, the handler function returns that name. This pattern is useful for creating type-safe APIs with a predefined set of valid values.\n\n@rt(\"/files/{path}\")\nasync def get(path: Path): return path.with_suffix('.txt')\nprint(cli.get('/files/foo').text)\n\nfoo.txt\n\n\nHandler functions can use Path objects as parameter types. The Path type is from Python’s standard library pathlib module, which provides an object-oriented interface for working with file paths. In this example, the path parameter is annotated with Path, so FastHTML automatically converts the string from the URL to a Path object.\nThis approach is particularly useful when working with file-related routes, as it provides a convenient and platform-independent way to handle file paths.\n\nfake_db = [{\"name\": \"Foo\"}, {\"name\": \"Bar\"}]\n\n@rt(\"/items/\")\ndef get(idx:int|None = 0): return fake_db[idx]\nprint(cli.get('/items/?idx=1').text)\n\n{\"name\":\"Bar\"}\n\n\nHandler functions can use query parameters, which are automatically parsed from the URL. In this example, idx is a query parameter with a default value of 0. It’s annotated as int|None, allowing it to be either an integer or None.\nThe function uses this parameter to index into a fake database (fake_db). When a request is made with a valid idx query parameter, the handler returns the corresponding item from the database.\n\nprint(cli.get('/items/').text)\n\n{\"name\":\"Foo\"}\n\n\nWhen no idx query parameter is provided, the handler function uses the default value of 0. This results in returning the first item from the fake_db list, which is {\"name\":\"Foo\"}.\nThis behavior demonstrates how default values for query parameters work in FastHTML. They allow the API to have a sensible default behavior when optional parameters are not provided.\n\nprint(cli.get('/items/?idx=g'))\n\n<Response [404 Not Found]>\n\n\nWhen an invalid value is provided for a typed query parameter, FastHTML returns a 404 Not Found response. In this example, ‘g’ is not a valid integer for the idx parameter, so the request fails with a 404 status.\nThis behavior ensures type safety and prevents invalid inputs from reaching the handler function.\n\n@app.get(\"/booly/\")\ndef _(coming:bool=True): return 'Coming' if coming else 'Not coming'\nprint(cli.get('/booly/?coming=true').text)\nprint(cli.get('/booly/?coming=no').text)\n\nComing\nNot coming\n\n\nHandler functions can use boolean query parameters. In this example, coming is a boolean parameter with a default value of True. FastHTML automatically converts string values like ‘true’, ‘false’, ‘1’, ‘0’, ‘on’, ‘off’, ‘yes’, and ‘no’ to their corresponding boolean values.\nThe underscore _ is used as the function name in this example to indicate that the function’s name is not important or won’t be referenced elsewhere. This is a common Python convention for throwaway or unused variables, and it works here because FastHTML uses the route decorator parameter, when provided, to determine the URL path, not the function name. By default, both get and post methods can be used in routes that don’t specify an http method (by either using app.get, def get, or the methods parameter to app.route).\n\n@app.get(\"/datie/\")\ndef _(d:parsed_date): return d\ndate_str = \"17th of May, 2024, 2p\"\nprint(cli.get(f'/datie/?d={date_str}').text)\n\n2024-05-17 14:00:00\n\n\nHandler functions can use date objects as parameter types. FastHTML uses dateutil.parser library to automatically parse a wide variety of date string formats into date objects.\n\n@app.get(\"/ua\")\nasync def _(user_agent:str): return user_agent\nprint(cli.get('/ua', headers={'User-Agent':'FastHTML'}).text)\n\nFastHTML\n\n\nHandler functions can access HTTP headers by using parameter names that match the header names. In this example, user_agent is used as a parameter name, which automatically captures the value of the ‘User-Agent’ header from the request.\nThe Client instance allows setting custom headers for test requests. Here, we set the ‘User-Agent’ header to ‘FastHTML’ in the test request.\n\n@app.get(\"/hxtest\")\ndef _(htmx): return htmx.request\nprint(cli.get('/hxtest', headers={'HX-Request':'1'}).text)\n\n@app.get(\"/hxtest2\")\ndef _(foo:HtmxHeaders, req): return foo.request\nprint(cli.get('/hxtest2', headers={'HX-Request':'1'}).text)\n\n1\n1\n\n\nHandler functions can access HTMX-specific headers using either the special htmx parameter name, or a parameter annotated with HtmxHeaders. Both approaches provide access to HTMX-related information.\nIn these examples, the htmx.request attribute returns the value of the ‘HX-Request’ header.\n\napp.chk = 'foo'\n@app.get(\"/app\")\ndef _(app): return app.chk\nprint(cli.get('/app').text)\n\nfoo\n\n\nHandler functions can access the FastHTML application instance using the special app parameter name. This allows handlers to access application-level attributes and methods.\nIn this example, we set a custom attribute chk on the application instance. The handler function then uses the app parameter to access this attribute and return its value.\n\n@app.get(\"/app2\")\ndef _(foo:FastHTML): return foo.chk,HttpHeader(\"mykey\", \"myval\")\nr = cli.get('/app2', **hxhdr)\nprint(r.text)\nprint(r.headers)\n\nfoo\n\nHeaders({'mykey': 'myval', 'content-length': '4', 'content-type': 'text/html; charset=utf-8'})\n\n\nHandler functions can access the FastHTML application instance using a parameter annotated with FastHTML. This allows handlers to access application-level attributes and methods, just like using the special app parameter name.\nHandlers can return tuples containing both content and HttpHeader objects. HttpHeader allows setting custom HTTP headers in the response.\nIn this example:\n\nWe define a handler that returns both the chk attribute from the application and a custom header.\nThe HttpHeader(\"mykey\", \"myval\") sets a custom header in the response.\nWe use the test client to make a request and examine both the response text and headers.\nThe response includes the custom header “mykey” along with standard headers like content-length and content-type.\n\n\n@app.get(\"/app3\")\ndef _(foo:FastHTML): return HtmxResponseHeaders(location=\"http://example.org\")\nr = cli.get('/app3')\nprint(r.headers)\n\nHeaders({'hx-location': 'http://example.org', 'content-length': '0', 'content-type': 'text/html; charset=utf-8'})\n\n\nHandler functions can return HtmxResponseHeaders objects to set HTMX-specific response headers. This is useful for HTMX-specific behaviors like client-side redirects.\nIn this example we define a handler that returns an HtmxResponseHeaders object with a location parameter, which sets the HX-Location header in the response. HTMX uses this for client-side redirects.\n\n@app.get(\"/app4\")\ndef _(foo:FastHTML): return Redirect(\"http://example.org\")\ncli.get('/app4', follow_redirects=False)\n\n<Response [303 See Other]>\n\n\nHandler functions can return Redirect objects to perform HTTP redirects. This is useful for redirecting users to different pages or external URLs.\nIn this example:\n\nWe define a handler that returns a Redirect object with the URL “http://example.org”.\nThe cli.get('/app4', follow_redirects=False) call simulates a GET request to the ‘/app4’ route without following redirects.\nThe response has a 303 See Other status code, indicating a redirect.\n\nThe follow_redirects=False parameter is used to prevent the test client from automatically following the redirect, allowing us to inspect the redirect response itself.\n\nRedirect.__response__\n\n<function fasthtml.core.Redirect.__response__(self, req)>\n\n\nThe Redirect class in FastHTML implements a __response__ method, which is a special method recognized by the framework. When a handler returns a Redirect object, FastHTML internally calls this __response__ method to replace the original response.\nThe __response__ method takes a req parameter, which represents the incoming request. This allows the method to access request information if needed when constructing the redirect response.\n\n@rt\ndef meta(): \n return ((Title('hi'),H1('hi')),\n (Meta(property='image'), Meta(property='site_name')))\n\nprint(cli.post('/meta').text)\n\n <!doctype html>\n\n <html>\n <head>\n <title>hi</title>\n <meta property=\"image\">\n <meta property=\"site_name\">\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n <script src=\"https://unpkg.com/htmx.org@next/dist/htmx.min.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@main/fasthtml.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js\"></script>\n </head>\n <body>\n <h1>hi</h1>\n </body>\n </html>\n\n\n\nFastHTML automatically identifies elements typically placed in the <head> (like Title and Meta) and positions them accordingly, while other elements go in the <body>.\nIn this example: - (Title('hi'), H1('hi')) defines the title and main heading. The title is placed in the head, and the H1 in the body. - (Meta(property='image'), Meta(property='site_name')) defines two meta tags, which are both placed in the head.",
+ "crumbs": [
+ "Home",
+ "Reference",
+ "Handling handlers"
+ ]
+ },
+ {
+ "objectID": "ref/handlers.html#form-data-and-json-handling",
+ "href": "ref/handlers.html#form-data-and-json-handling",
+ "title": "Handling handlers",
+ "section": "Form Data and JSON Handling",
+ "text": "Form Data and JSON Handling\n\n@app.post('/profile/me')\ndef profile_update(username: str): return username\n\nprint(cli.post('/profile/me', data={'username' : 'Alexis'}).text)\nr = cli.post('/profile/me', data={})\nprint(r.text)\nr\n\nAlexis\nMissing required field: username\n\n\n<Response [400 Bad Request]>\n\n\nHandler functions can accept form data parameters, without needing to manually extract it from the request. In this example, username is expected to be sent as form data.\nIf required form data is missing, FastHTML automatically returns a 400 Bad Request response with an error message.\nThe data parameter in the cli.post() method simulates sending form data in the request.\n\n@app.post('/pet/dog')\ndef pet_dog(dogname: str = None): return dogname or 'unknown name'\nprint(cli.post('/pet/dog', data={}).text)\n\nunknown name\n\n\nHandlers can have optional form data parameters with default values. In this example, dogname is an optional parameter with a default value of None.\nHere, if the form data doesn’t include the dogname field, the function uses the default value. The function returns either the provided dogname or ‘unknown name’ if dogname is None.\n\n@dataclass\nclass Bodie: a:int;b:str\n\n@rt(\"/bodie/{nm}\")\ndef post(nm:str, data:Bodie):\n res = asdict(data)\n res['nm'] = nm\n return res\n\nprint(cli.post('/bodie/me', data=dict(a=1, b='foo', nm='me')).text)\n\n{\"a\":1,\"b\":\"foo\",\"nm\":\"me\"}\n\n\nYou can use dataclasses to define structured form data. In this example, Bodie is a dataclass with a (int) and b (str) fields.\nFastHTML automatically converts the incoming form data to a Bodie instance where attribute names match parameter names. Other form data elements are matched with parameters with the same names (in this case, nm).\nHandler functions can return dictionaries, which FastHTML automatically JSON-encodes.\n\n@app.post(\"/bodied/\")\ndef bodied(data:dict): return data\n\nd = dict(a=1, b='foo')\nprint(cli.post('/bodied/', data=d).text)\n\n{\"a\":\"1\",\"b\":\"foo\"}\n\n\ndict parameters capture all form data as a dictionary. In this example, the data parameter is annotated with dict, so FastHTML automatically converts all incoming form data into a dictionary.\nNote that when form data is converted to a dictionary, all values become strings, even if they were originally numbers. This is why the ‘a’ key in the response has a string value “1” instead of the integer 1.\n\nnt = namedtuple('Bodient', ['a','b'])\n\n@app.post(\"/bodient/\")\ndef bodient(data:nt): return asdict(data)\nprint(cli.post('/bodient/', data=d).text)\n\n{\"a\":\"1\",\"b\":\"foo\"}\n\n\nHandler functions can use named tuples to define structured form data. In this example, Bodient is a named tuple with a and b fields.\nFastHTML automatically converts the incoming form data to a Bodient instance where field names match parameter names. As with the previous example, all form data values are converted to strings in the process.\n\nclass BodieTD(TypedDict): a:int;b:str='foo'\n\n@app.post(\"/bodietd/\")\ndef bodient(data:BodieTD): return data\nprint(cli.post('/bodietd/', data=d).text)\n\n{\"a\":1,\"b\":\"foo\"}\n\n\nYou can use TypedDict to define structured form data with type hints. In this example, BodieTD is a TypedDict with a (int) and b (str) fields, where b has a default value of ‘foo’.\nFastHTML automatically converts the incoming form data to a BodieTD instance where keys match the defined fields. Unlike with regular dictionaries or named tuples, FastHTML respects the type hints in TypedDict, converting values to the specified types when possible (e.g., converting ‘1’ to the integer 1 for the ‘a’ field).\n\nclass Bodie2:\n a:int|None; b:str\n def __init__(self, a, b='foo'): store_attr()\n\n@app.post(\"/bodie2/\")\ndef bodie(d:Bodie2): return f\"a: {d.a}; b: {d.b}\"\nprint(cli.post('/bodie2/', data={'a':1}).text)\n\na: 1; b: foo\n\n\nCustom classes can be used to define structured form data. Here, Bodie2 is a custom class with a (int|None) and b (str) attributes, where b has a default value of ‘foo’. The store_attr() function (from fastcore) automatically assigns constructor parameters to instance attributes.\nFastHTML automatically converts the incoming form data to a Bodie2 instance, matching form fields to constructor parameters. It respects type hints and default values.\n\n@app.post(\"/b\")\ndef index(it: Bodie): return Titled(\"It worked!\", P(f\"{it.a}, {it.b}\"))\n\ns = json.dumps({\"b\": \"Lorem\", \"a\": 15})\nprint(cli.post('/b', headers={\"Content-Type\": \"application/json\", 'hx-request':\"1\"}, data=s).text)\n\n <title>It worked!</title>\n\n <main class=\"container\">\n <h1>It worked!</h1>\n <p>15, Lorem</p>\n </main>\n\n\n\nHandler functions can accept JSON data as input, which is automatically parsed into the specified type. In this example, it is of type Bodie, and FastHTML converts the incoming JSON data to a Bodie instance.\nThe Titled component is used to create a page with a title and main content. It automatically generates an <h1> with the provided title, wraps the content in a <main> tag with a “container” class, and adds a title to the head.\nWhen making a request with JSON data: - Set the “Content-Type” header to “application/json” - Provide the JSON data as a string in the data parameter of the request",
+ "crumbs": [
+ "Home",
+ "Reference",
+ "Handling handlers"
+ ]
+ },
+ {
+ "objectID": "ref/handlers.html#cookies-sessions-file-uploads-and-more",
+ "href": "ref/handlers.html#cookies-sessions-file-uploads-and-more",
+ "title": "Handling handlers",
+ "section": "Cookies, Sessions, File Uploads, and more",
+ "text": "Cookies, Sessions, File Uploads, and more\n\n@rt(\"/setcookie\")\ndef get(): return cookie('now', datetime.now())\n\n@rt(\"/getcookie\")\ndef get(now:parsed_date): return f'Cookie was set at time {now.time()}'\n\nprint(cli.get('/setcookie').text)\ntime.sleep(0.01)\ncli.get('/getcookie').text\n\n\n\n\n'Cookie was set at time 16:53:08.485345'\n\n\nHandler functions can set and retrieve cookies. In this example:\n\nThe /setcookie route sets a cookie named ‘now’ with the current datetime.\nThe /getcookie route retrieves the ‘now’ cookie and returns its value.\n\nThe cookie() function is used to create a cookie response. FastHTML automatically converts the datetime object to a string when setting the cookie, and parses it back to a date object when retrieving it.\n\ncookie('now', datetime.now())\n\nHttpHeader(k='set-cookie', v='now=\"2024-09-07 16:54:05.275757\"; Path=/; SameSite=lax')\n\n\nThe cookie() function returns an HttpHeader object with the ‘set-cookie’ key. You can return it in a tuple along with FT elements, along with anything else FastHTML supports in responses.\n\napp = FastHTML(secret_key='soopersecret')\ncli = Client(app)\nrt = app.route\n\n\n@rt(\"/setsess\")\ndef get(sess, foo:str=''):\n now = datetime.now()\n sess['auth'] = str(now)\n return f'Set to {now}'\n\n@rt(\"/getsess\")\ndef get(sess): return f'Session time: {sess[\"auth\"]}'\n\nprint(cli.get('/setsess').text)\ntime.sleep(0.01)\n\ncli.get('/getsess').text\n\nSet to 2024-09-07 16:56:20.445950\n\n\n'Session time: 2024-09-07 16:56:20.445950'\n\n\nSessions store and retrieve data across requests. To use sessions, you should to initialize the FastHTML application with a secret_key. This is used to cryptographically sign the cookie used by the session.\nThe sess parameter in handler functions provides access to the session data. You can set and get session variables using dictionary-style access.\n\n@rt(\"/upload\")\nasync def post(uf:UploadFile): return (await uf.read()).decode()\n\nwith open('../../CHANGELOG.md', 'rb') as f:\n print(cli.post('/upload', files={'uf':f}, data={'msg':'Hello'}).text[:15])\n\n# Release notes\n\n\nHandler functions can accept file uploads using Starlette’s UploadFile type. In this example:\n\nThe /upload route accepts a file upload named uf.\nThe UploadFile object provides an asynchronous read() method to access the file contents.\nWe use await to read the file content asynchronously and decode it to a string.\n\nWe added async to the handler function because it uses await to read the file content asynchronously. In Python, any function that uses await must be declared as async. This allows the function to be run asynchronously, potentially improving performance by not blocking other operations while waiting for the file to be read.\n\napp.static_route('.md', static_path='../..')\nprint(cli.get('/README.md').text[:10])\n\n# FastHTML\n\n\nThe static_route method of the FastHTML application allows serving static files with specified extensions from a given directory. In this example:\n\n.md files are served from the ../.. directory (two levels up from the current directory).\nAccessing /README.md returns the contents of the README.md file from that directory.\n\n\nhelp(app.static_route_exts)\n\nHelp on method static_route_exts in module fasthtml.core:\n\nstatic_route_exts(prefix='/', static_path='.', exts='static') method of fasthtml.core.FastHTML instance\n Add a static route at URL path `prefix` with files from `static_path` and `exts` defined by `reg_re_param()`\n\n\n\n\napp.static_route_exts()\nprint(cli.get('/README.txt').text[:50])\n\nThese are the source notebooks for FastHTML.\n\n\n\n\nThe static_route_exts method of the FastHTML application allows serving static files with specified extensions from a given directory. By default:\n\nIt serves files from the current directory (‘.’).\nIt uses the ‘static’ regex, which includes common static file extensions like ‘ico’, ‘gif’, ‘jpg’, ‘css’, ‘js’, etc.\nThe URL prefix is set to ‘/’.\n\nThe ‘static’ regex is defined by FastHTML using this code:\nreg_re_param(\"static\", \"ico|gif|jpg|jpeg|webm|css|js|woff|png|svg|mp4|webp|ttf|otf|eot|woff2|txt|html|map\")\n\n@rt(\"/form-submit/{list_id}\")\ndef options(list_id: str):\n headers = {\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Methods': 'POST',\n 'Access-Control-Allow-Headers': '*',\n }\n return Response(status_code=200, headers=headers)\n\nprint(cli.options('/form-submit/2').headers)\n\nHeaders({'access-control-allow-origin': '*', 'access-control-allow-methods': 'POST', 'access-control-allow-headers': '*', 'content-length': '0', 'set-cookie': 'session_=eyJhdXRoIjogIjIwMjQtMDktMDcgMTY6NTY6MjAuNDQ1OTUwIiwgIm5hbWUiOiAiMiJ9.Ztv60A.N0b4z5rNg6GT3xCFc8X4DqwcNjQ; path=/; Max-Age=31536000; httponly; samesite=lax'})\n\n\nFastHTML handlers can handle OPTIONS requests and set custom headers. In this example:\n\nThe /form-submit/{list_id} route handles OPTIONS requests.\nCustom headers are set to allow cross-origin requests (CORS).\nThe function returns a Starlette Response object with a 200 status code and the custom headers.\n\nYou can return any Starlette Response type from a handler function, giving you full control over the response when needed.\n\ndef _not_found(req, exc): return Div('nope')\n\napp = FastHTML(exception_handlers={404:_not_found})\ncli = Client(app)\nrt = app.route\n\nr = cli.get('/')\nprint(r.text)\n\n <!doctype html>\n\n <html>\n <head>\n <title>FastHTML page</title>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n <script src=\"https://unpkg.com/htmx.org@next/dist/htmx.min.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@main/fasthtml.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js\"></script>\n </head>\n <body>\n <div>nope</div>\n </body>\n </html>\n\n\n\nFastHTML allows you to define custom exception handlers. In this example we defined a custom 404 (Not Found) handler function _not_found, which returns a Div component with the text ‘nope’.\nFastHTML includes the full HTML structure, since we returned an HTML partial.",
+ "crumbs": [
+ "Home",
+ "Reference",
+ "Handling handlers"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/site_libs/bootstrap/bootstrap-icons.css b/site_libs/bootstrap/bootstrap-icons.css
new file mode 100644
index 00000000..285e4448
--- /dev/null
+++ b/site_libs/bootstrap/bootstrap-icons.css
@@ -0,0 +1,2078 @@
+/*!
+ * Bootstrap Icons v1.11.1 (https://icons.getbootstrap.com/)
+ * Copyright 2019-2023 The Bootstrap Authors
+ * Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE)
+ */
+
+@font-face {
+ font-display: block;
+ font-family: "bootstrap-icons";
+ src:
+url("./bootstrap-icons.woff?2820a3852bdb9a5832199cc61cec4e65") format("woff");
+}
+
+.bi::before,
+[class^="bi-"]::before,
+[class*=" bi-"]::before {
+ display: inline-block;
+ font-family: bootstrap-icons !important;
+ font-style: normal;
+ font-weight: normal !important;
+ font-variant: normal;
+ text-transform: none;
+ line-height: 1;
+ vertical-align: -.125em;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.bi-123::before { content: "\f67f"; }
+.bi-alarm-fill::before { content: "\f101"; }
+.bi-alarm::before { content: "\f102"; }
+.bi-align-bottom::before { content: "\f103"; }
+.bi-align-center::before { content: "\f104"; }
+.bi-align-end::before { content: "\f105"; }
+.bi-align-middle::before { content: "\f106"; }
+.bi-align-start::before { content: "\f107"; }
+.bi-align-top::before { content: "\f108"; }
+.bi-alt::before { content: "\f109"; }
+.bi-app-indicator::before { content: "\f10a"; }
+.bi-app::before { content: "\f10b"; }
+.bi-archive-fill::before { content: "\f10c"; }
+.bi-archive::before { content: "\f10d"; }
+.bi-arrow-90deg-down::before { content: "\f10e"; }
+.bi-arrow-90deg-left::before { content: "\f10f"; }
+.bi-arrow-90deg-right::before { content: "\f110"; }
+.bi-arrow-90deg-up::before { content: "\f111"; }
+.bi-arrow-bar-down::before { content: "\f112"; }
+.bi-arrow-bar-left::before { content: "\f113"; }
+.bi-arrow-bar-right::before { content: "\f114"; }
+.bi-arrow-bar-up::before { content: "\f115"; }
+.bi-arrow-clockwise::before { content: "\f116"; }
+.bi-arrow-counterclockwise::before { content: "\f117"; }
+.bi-arrow-down-circle-fill::before { content: "\f118"; }
+.bi-arrow-down-circle::before { content: "\f119"; }
+.bi-arrow-down-left-circle-fill::before { content: "\f11a"; }
+.bi-arrow-down-left-circle::before { content: "\f11b"; }
+.bi-arrow-down-left-square-fill::before { content: "\f11c"; }
+.bi-arrow-down-left-square::before { content: "\f11d"; }
+.bi-arrow-down-left::before { content: "\f11e"; }
+.bi-arrow-down-right-circle-fill::before { content: "\f11f"; }
+.bi-arrow-down-right-circle::before { content: "\f120"; }
+.bi-arrow-down-right-square-fill::before { content: "\f121"; }
+.bi-arrow-down-right-square::before { content: "\f122"; }
+.bi-arrow-down-right::before { content: "\f123"; }
+.bi-arrow-down-short::before { content: "\f124"; }
+.bi-arrow-down-square-fill::before { content: "\f125"; }
+.bi-arrow-down-square::before { content: "\f126"; }
+.bi-arrow-down-up::before { content: "\f127"; }
+.bi-arrow-down::before { content: "\f128"; }
+.bi-arrow-left-circle-fill::before { content: "\f129"; }
+.bi-arrow-left-circle::before { content: "\f12a"; }
+.bi-arrow-left-right::before { content: "\f12b"; }
+.bi-arrow-left-short::before { content: "\f12c"; }
+.bi-arrow-left-square-fill::before { content: "\f12d"; }
+.bi-arrow-left-square::before { content: "\f12e"; }
+.bi-arrow-left::before { content: "\f12f"; }
+.bi-arrow-repeat::before { content: "\f130"; }
+.bi-arrow-return-left::before { content: "\f131"; }
+.bi-arrow-return-right::before { content: "\f132"; }
+.bi-arrow-right-circle-fill::before { content: "\f133"; }
+.bi-arrow-right-circle::before { content: "\f134"; }
+.bi-arrow-right-short::before { content: "\f135"; }
+.bi-arrow-right-square-fill::before { content: "\f136"; }
+.bi-arrow-right-square::before { content: "\f137"; }
+.bi-arrow-right::before { content: "\f138"; }
+.bi-arrow-up-circle-fill::before { content: "\f139"; }
+.bi-arrow-up-circle::before { content: "\f13a"; }
+.bi-arrow-up-left-circle-fill::before { content: "\f13b"; }
+.bi-arrow-up-left-circle::before { content: "\f13c"; }
+.bi-arrow-up-left-square-fill::before { content: "\f13d"; }
+.bi-arrow-up-left-square::before { content: "\f13e"; }
+.bi-arrow-up-left::before { content: "\f13f"; }
+.bi-arrow-up-right-circle-fill::before { content: "\f140"; }
+.bi-arrow-up-right-circle::before { content: "\f141"; }
+.bi-arrow-up-right-square-fill::before { content: "\f142"; }
+.bi-arrow-up-right-square::before { content: "\f143"; }
+.bi-arrow-up-right::before { content: "\f144"; }
+.bi-arrow-up-short::before { content: "\f145"; }
+.bi-arrow-up-square-fill::before { content: "\f146"; }
+.bi-arrow-up-square::before { content: "\f147"; }
+.bi-arrow-up::before { content: "\f148"; }
+.bi-arrows-angle-contract::before { content: "\f149"; }
+.bi-arrows-angle-expand::before { content: "\f14a"; }
+.bi-arrows-collapse::before { content: "\f14b"; }
+.bi-arrows-expand::before { content: "\f14c"; }
+.bi-arrows-fullscreen::before { content: "\f14d"; }
+.bi-arrows-move::before { content: "\f14e"; }
+.bi-aspect-ratio-fill::before { content: "\f14f"; }
+.bi-aspect-ratio::before { content: "\f150"; }
+.bi-asterisk::before { content: "\f151"; }
+.bi-at::before { content: "\f152"; }
+.bi-award-fill::before { content: "\f153"; }
+.bi-award::before { content: "\f154"; }
+.bi-back::before { content: "\f155"; }
+.bi-backspace-fill::before { content: "\f156"; }
+.bi-backspace-reverse-fill::before { content: "\f157"; }
+.bi-backspace-reverse::before { content: "\f158"; }
+.bi-backspace::before { content: "\f159"; }
+.bi-badge-3d-fill::before { content: "\f15a"; }
+.bi-badge-3d::before { content: "\f15b"; }
+.bi-badge-4k-fill::before { content: "\f15c"; }
+.bi-badge-4k::before { content: "\f15d"; }
+.bi-badge-8k-fill::before { content: "\f15e"; }
+.bi-badge-8k::before { content: "\f15f"; }
+.bi-badge-ad-fill::before { content: "\f160"; }
+.bi-badge-ad::before { content: "\f161"; }
+.bi-badge-ar-fill::before { content: "\f162"; }
+.bi-badge-ar::before { content: "\f163"; }
+.bi-badge-cc-fill::before { content: "\f164"; }
+.bi-badge-cc::before { content: "\f165"; }
+.bi-badge-hd-fill::before { content: "\f166"; }
+.bi-badge-hd::before { content: "\f167"; }
+.bi-badge-tm-fill::before { content: "\f168"; }
+.bi-badge-tm::before { content: "\f169"; }
+.bi-badge-vo-fill::before { content: "\f16a"; }
+.bi-badge-vo::before { content: "\f16b"; }
+.bi-badge-vr-fill::before { content: "\f16c"; }
+.bi-badge-vr::before { content: "\f16d"; }
+.bi-badge-wc-fill::before { content: "\f16e"; }
+.bi-badge-wc::before { content: "\f16f"; }
+.bi-bag-check-fill::before { content: "\f170"; }
+.bi-bag-check::before { content: "\f171"; }
+.bi-bag-dash-fill::before { content: "\f172"; }
+.bi-bag-dash::before { content: "\f173"; }
+.bi-bag-fill::before { content: "\f174"; }
+.bi-bag-plus-fill::before { content: "\f175"; }
+.bi-bag-plus::before { content: "\f176"; }
+.bi-bag-x-fill::before { content: "\f177"; }
+.bi-bag-x::before { content: "\f178"; }
+.bi-bag::before { content: "\f179"; }
+.bi-bar-chart-fill::before { content: "\f17a"; }
+.bi-bar-chart-line-fill::before { content: "\f17b"; }
+.bi-bar-chart-line::before { content: "\f17c"; }
+.bi-bar-chart-steps::before { content: "\f17d"; }
+.bi-bar-chart::before { content: "\f17e"; }
+.bi-basket-fill::before { content: "\f17f"; }
+.bi-basket::before { content: "\f180"; }
+.bi-basket2-fill::before { content: "\f181"; }
+.bi-basket2::before { content: "\f182"; }
+.bi-basket3-fill::before { content: "\f183"; }
+.bi-basket3::before { content: "\f184"; }
+.bi-battery-charging::before { content: "\f185"; }
+.bi-battery-full::before { content: "\f186"; }
+.bi-battery-half::before { content: "\f187"; }
+.bi-battery::before { content: "\f188"; }
+.bi-bell-fill::before { content: "\f189"; }
+.bi-bell::before { content: "\f18a"; }
+.bi-bezier::before { content: "\f18b"; }
+.bi-bezier2::before { content: "\f18c"; }
+.bi-bicycle::before { content: "\f18d"; }
+.bi-binoculars-fill::before { content: "\f18e"; }
+.bi-binoculars::before { content: "\f18f"; }
+.bi-blockquote-left::before { content: "\f190"; }
+.bi-blockquote-right::before { content: "\f191"; }
+.bi-book-fill::before { content: "\f192"; }
+.bi-book-half::before { content: "\f193"; }
+.bi-book::before { content: "\f194"; }
+.bi-bookmark-check-fill::before { content: "\f195"; }
+.bi-bookmark-check::before { content: "\f196"; }
+.bi-bookmark-dash-fill::before { content: "\f197"; }
+.bi-bookmark-dash::before { content: "\f198"; }
+.bi-bookmark-fill::before { content: "\f199"; }
+.bi-bookmark-heart-fill::before { content: "\f19a"; }
+.bi-bookmark-heart::before { content: "\f19b"; }
+.bi-bookmark-plus-fill::before { content: "\f19c"; }
+.bi-bookmark-plus::before { content: "\f19d"; }
+.bi-bookmark-star-fill::before { content: "\f19e"; }
+.bi-bookmark-star::before { content: "\f19f"; }
+.bi-bookmark-x-fill::before { content: "\f1a0"; }
+.bi-bookmark-x::before { content: "\f1a1"; }
+.bi-bookmark::before { content: "\f1a2"; }
+.bi-bookmarks-fill::before { content: "\f1a3"; }
+.bi-bookmarks::before { content: "\f1a4"; }
+.bi-bookshelf::before { content: "\f1a5"; }
+.bi-bootstrap-fill::before { content: "\f1a6"; }
+.bi-bootstrap-reboot::before { content: "\f1a7"; }
+.bi-bootstrap::before { content: "\f1a8"; }
+.bi-border-all::before { content: "\f1a9"; }
+.bi-border-bottom::before { content: "\f1aa"; }
+.bi-border-center::before { content: "\f1ab"; }
+.bi-border-inner::before { content: "\f1ac"; }
+.bi-border-left::before { content: "\f1ad"; }
+.bi-border-middle::before { content: "\f1ae"; }
+.bi-border-outer::before { content: "\f1af"; }
+.bi-border-right::before { content: "\f1b0"; }
+.bi-border-style::before { content: "\f1b1"; }
+.bi-border-top::before { content: "\f1b2"; }
+.bi-border-width::before { content: "\f1b3"; }
+.bi-border::before { content: "\f1b4"; }
+.bi-bounding-box-circles::before { content: "\f1b5"; }
+.bi-bounding-box::before { content: "\f1b6"; }
+.bi-box-arrow-down-left::before { content: "\f1b7"; }
+.bi-box-arrow-down-right::before { content: "\f1b8"; }
+.bi-box-arrow-down::before { content: "\f1b9"; }
+.bi-box-arrow-in-down-left::before { content: "\f1ba"; }
+.bi-box-arrow-in-down-right::before { content: "\f1bb"; }
+.bi-box-arrow-in-down::before { content: "\f1bc"; }
+.bi-box-arrow-in-left::before { content: "\f1bd"; }
+.bi-box-arrow-in-right::before { content: "\f1be"; }
+.bi-box-arrow-in-up-left::before { content: "\f1bf"; }
+.bi-box-arrow-in-up-right::before { content: "\f1c0"; }
+.bi-box-arrow-in-up::before { content: "\f1c1"; }
+.bi-box-arrow-left::before { content: "\f1c2"; }
+.bi-box-arrow-right::before { content: "\f1c3"; }
+.bi-box-arrow-up-left::before { content: "\f1c4"; }
+.bi-box-arrow-up-right::before { content: "\f1c5"; }
+.bi-box-arrow-up::before { content: "\f1c6"; }
+.bi-box-seam::before { content: "\f1c7"; }
+.bi-box::before { content: "\f1c8"; }
+.bi-braces::before { content: "\f1c9"; }
+.bi-bricks::before { content: "\f1ca"; }
+.bi-briefcase-fill::before { content: "\f1cb"; }
+.bi-briefcase::before { content: "\f1cc"; }
+.bi-brightness-alt-high-fill::before { content: "\f1cd"; }
+.bi-brightness-alt-high::before { content: "\f1ce"; }
+.bi-brightness-alt-low-fill::before { content: "\f1cf"; }
+.bi-brightness-alt-low::before { content: "\f1d0"; }
+.bi-brightness-high-fill::before { content: "\f1d1"; }
+.bi-brightness-high::before { content: "\f1d2"; }
+.bi-brightness-low-fill::before { content: "\f1d3"; }
+.bi-brightness-low::before { content: "\f1d4"; }
+.bi-broadcast-pin::before { content: "\f1d5"; }
+.bi-broadcast::before { content: "\f1d6"; }
+.bi-brush-fill::before { content: "\f1d7"; }
+.bi-brush::before { content: "\f1d8"; }
+.bi-bucket-fill::before { content: "\f1d9"; }
+.bi-bucket::before { content: "\f1da"; }
+.bi-bug-fill::before { content: "\f1db"; }
+.bi-bug::before { content: "\f1dc"; }
+.bi-building::before { content: "\f1dd"; }
+.bi-bullseye::before { content: "\f1de"; }
+.bi-calculator-fill::before { content: "\f1df"; }
+.bi-calculator::before { content: "\f1e0"; }
+.bi-calendar-check-fill::before { content: "\f1e1"; }
+.bi-calendar-check::before { content: "\f1e2"; }
+.bi-calendar-date-fill::before { content: "\f1e3"; }
+.bi-calendar-date::before { content: "\f1e4"; }
+.bi-calendar-day-fill::before { content: "\f1e5"; }
+.bi-calendar-day::before { content: "\f1e6"; }
+.bi-calendar-event-fill::before { content: "\f1e7"; }
+.bi-calendar-event::before { content: "\f1e8"; }
+.bi-calendar-fill::before { content: "\f1e9"; }
+.bi-calendar-minus-fill::before { content: "\f1ea"; }
+.bi-calendar-minus::before { content: "\f1eb"; }
+.bi-calendar-month-fill::before { content: "\f1ec"; }
+.bi-calendar-month::before { content: "\f1ed"; }
+.bi-calendar-plus-fill::before { content: "\f1ee"; }
+.bi-calendar-plus::before { content: "\f1ef"; }
+.bi-calendar-range-fill::before { content: "\f1f0"; }
+.bi-calendar-range::before { content: "\f1f1"; }
+.bi-calendar-week-fill::before { content: "\f1f2"; }
+.bi-calendar-week::before { content: "\f1f3"; }
+.bi-calendar-x-fill::before { content: "\f1f4"; }
+.bi-calendar-x::before { content: "\f1f5"; }
+.bi-calendar::before { content: "\f1f6"; }
+.bi-calendar2-check-fill::before { content: "\f1f7"; }
+.bi-calendar2-check::before { content: "\f1f8"; }
+.bi-calendar2-date-fill::before { content: "\f1f9"; }
+.bi-calendar2-date::before { content: "\f1fa"; }
+.bi-calendar2-day-fill::before { content: "\f1fb"; }
+.bi-calendar2-day::before { content: "\f1fc"; }
+.bi-calendar2-event-fill::before { content: "\f1fd"; }
+.bi-calendar2-event::before { content: "\f1fe"; }
+.bi-calendar2-fill::before { content: "\f1ff"; }
+.bi-calendar2-minus-fill::before { content: "\f200"; }
+.bi-calendar2-minus::before { content: "\f201"; }
+.bi-calendar2-month-fill::before { content: "\f202"; }
+.bi-calendar2-month::before { content: "\f203"; }
+.bi-calendar2-plus-fill::before { content: "\f204"; }
+.bi-calendar2-plus::before { content: "\f205"; }
+.bi-calendar2-range-fill::before { content: "\f206"; }
+.bi-calendar2-range::before { content: "\f207"; }
+.bi-calendar2-week-fill::before { content: "\f208"; }
+.bi-calendar2-week::before { content: "\f209"; }
+.bi-calendar2-x-fill::before { content: "\f20a"; }
+.bi-calendar2-x::before { content: "\f20b"; }
+.bi-calendar2::before { content: "\f20c"; }
+.bi-calendar3-event-fill::before { content: "\f20d"; }
+.bi-calendar3-event::before { content: "\f20e"; }
+.bi-calendar3-fill::before { content: "\f20f"; }
+.bi-calendar3-range-fill::before { content: "\f210"; }
+.bi-calendar3-range::before { content: "\f211"; }
+.bi-calendar3-week-fill::before { content: "\f212"; }
+.bi-calendar3-week::before { content: "\f213"; }
+.bi-calendar3::before { content: "\f214"; }
+.bi-calendar4-event::before { content: "\f215"; }
+.bi-calendar4-range::before { content: "\f216"; }
+.bi-calendar4-week::before { content: "\f217"; }
+.bi-calendar4::before { content: "\f218"; }
+.bi-camera-fill::before { content: "\f219"; }
+.bi-camera-reels-fill::before { content: "\f21a"; }
+.bi-camera-reels::before { content: "\f21b"; }
+.bi-camera-video-fill::before { content: "\f21c"; }
+.bi-camera-video-off-fill::before { content: "\f21d"; }
+.bi-camera-video-off::before { content: "\f21e"; }
+.bi-camera-video::before { content: "\f21f"; }
+.bi-camera::before { content: "\f220"; }
+.bi-camera2::before { content: "\f221"; }
+.bi-capslock-fill::before { content: "\f222"; }
+.bi-capslock::before { content: "\f223"; }
+.bi-card-checklist::before { content: "\f224"; }
+.bi-card-heading::before { content: "\f225"; }
+.bi-card-image::before { content: "\f226"; }
+.bi-card-list::before { content: "\f227"; }
+.bi-card-text::before { content: "\f228"; }
+.bi-caret-down-fill::before { content: "\f229"; }
+.bi-caret-down-square-fill::before { content: "\f22a"; }
+.bi-caret-down-square::before { content: "\f22b"; }
+.bi-caret-down::before { content: "\f22c"; }
+.bi-caret-left-fill::before { content: "\f22d"; }
+.bi-caret-left-square-fill::before { content: "\f22e"; }
+.bi-caret-left-square::before { content: "\f22f"; }
+.bi-caret-left::before { content: "\f230"; }
+.bi-caret-right-fill::before { content: "\f231"; }
+.bi-caret-right-square-fill::before { content: "\f232"; }
+.bi-caret-right-square::before { content: "\f233"; }
+.bi-caret-right::before { content: "\f234"; }
+.bi-caret-up-fill::before { content: "\f235"; }
+.bi-caret-up-square-fill::before { content: "\f236"; }
+.bi-caret-up-square::before { content: "\f237"; }
+.bi-caret-up::before { content: "\f238"; }
+.bi-cart-check-fill::before { content: "\f239"; }
+.bi-cart-check::before { content: "\f23a"; }
+.bi-cart-dash-fill::before { content: "\f23b"; }
+.bi-cart-dash::before { content: "\f23c"; }
+.bi-cart-fill::before { content: "\f23d"; }
+.bi-cart-plus-fill::before { content: "\f23e"; }
+.bi-cart-plus::before { content: "\f23f"; }
+.bi-cart-x-fill::before { content: "\f240"; }
+.bi-cart-x::before { content: "\f241"; }
+.bi-cart::before { content: "\f242"; }
+.bi-cart2::before { content: "\f243"; }
+.bi-cart3::before { content: "\f244"; }
+.bi-cart4::before { content: "\f245"; }
+.bi-cash-stack::before { content: "\f246"; }
+.bi-cash::before { content: "\f247"; }
+.bi-cast::before { content: "\f248"; }
+.bi-chat-dots-fill::before { content: "\f249"; }
+.bi-chat-dots::before { content: "\f24a"; }
+.bi-chat-fill::before { content: "\f24b"; }
+.bi-chat-left-dots-fill::before { content: "\f24c"; }
+.bi-chat-left-dots::before { content: "\f24d"; }
+.bi-chat-left-fill::before { content: "\f24e"; }
+.bi-chat-left-quote-fill::before { content: "\f24f"; }
+.bi-chat-left-quote::before { content: "\f250"; }
+.bi-chat-left-text-fill::before { content: "\f251"; }
+.bi-chat-left-text::before { content: "\f252"; }
+.bi-chat-left::before { content: "\f253"; }
+.bi-chat-quote-fill::before { content: "\f254"; }
+.bi-chat-quote::before { content: "\f255"; }
+.bi-chat-right-dots-fill::before { content: "\f256"; }
+.bi-chat-right-dots::before { content: "\f257"; }
+.bi-chat-right-fill::before { content: "\f258"; }
+.bi-chat-right-quote-fill::before { content: "\f259"; }
+.bi-chat-right-quote::before { content: "\f25a"; }
+.bi-chat-right-text-fill::before { content: "\f25b"; }
+.bi-chat-right-text::before { content: "\f25c"; }
+.bi-chat-right::before { content: "\f25d"; }
+.bi-chat-square-dots-fill::before { content: "\f25e"; }
+.bi-chat-square-dots::before { content: "\f25f"; }
+.bi-chat-square-fill::before { content: "\f260"; }
+.bi-chat-square-quote-fill::before { content: "\f261"; }
+.bi-chat-square-quote::before { content: "\f262"; }
+.bi-chat-square-text-fill::before { content: "\f263"; }
+.bi-chat-square-text::before { content: "\f264"; }
+.bi-chat-square::before { content: "\f265"; }
+.bi-chat-text-fill::before { content: "\f266"; }
+.bi-chat-text::before { content: "\f267"; }
+.bi-chat::before { content: "\f268"; }
+.bi-check-all::before { content: "\f269"; }
+.bi-check-circle-fill::before { content: "\f26a"; }
+.bi-check-circle::before { content: "\f26b"; }
+.bi-check-square-fill::before { content: "\f26c"; }
+.bi-check-square::before { content: "\f26d"; }
+.bi-check::before { content: "\f26e"; }
+.bi-check2-all::before { content: "\f26f"; }
+.bi-check2-circle::before { content: "\f270"; }
+.bi-check2-square::before { content: "\f271"; }
+.bi-check2::before { content: "\f272"; }
+.bi-chevron-bar-contract::before { content: "\f273"; }
+.bi-chevron-bar-down::before { content: "\f274"; }
+.bi-chevron-bar-expand::before { content: "\f275"; }
+.bi-chevron-bar-left::before { content: "\f276"; }
+.bi-chevron-bar-right::before { content: "\f277"; }
+.bi-chevron-bar-up::before { content: "\f278"; }
+.bi-chevron-compact-down::before { content: "\f279"; }
+.bi-chevron-compact-left::before { content: "\f27a"; }
+.bi-chevron-compact-right::before { content: "\f27b"; }
+.bi-chevron-compact-up::before { content: "\f27c"; }
+.bi-chevron-contract::before { content: "\f27d"; }
+.bi-chevron-double-down::before { content: "\f27e"; }
+.bi-chevron-double-left::before { content: "\f27f"; }
+.bi-chevron-double-right::before { content: "\f280"; }
+.bi-chevron-double-up::before { content: "\f281"; }
+.bi-chevron-down::before { content: "\f282"; }
+.bi-chevron-expand::before { content: "\f283"; }
+.bi-chevron-left::before { content: "\f284"; }
+.bi-chevron-right::before { content: "\f285"; }
+.bi-chevron-up::before { content: "\f286"; }
+.bi-circle-fill::before { content: "\f287"; }
+.bi-circle-half::before { content: "\f288"; }
+.bi-circle-square::before { content: "\f289"; }
+.bi-circle::before { content: "\f28a"; }
+.bi-clipboard-check::before { content: "\f28b"; }
+.bi-clipboard-data::before { content: "\f28c"; }
+.bi-clipboard-minus::before { content: "\f28d"; }
+.bi-clipboard-plus::before { content: "\f28e"; }
+.bi-clipboard-x::before { content: "\f28f"; }
+.bi-clipboard::before { content: "\f290"; }
+.bi-clock-fill::before { content: "\f291"; }
+.bi-clock-history::before { content: "\f292"; }
+.bi-clock::before { content: "\f293"; }
+.bi-cloud-arrow-down-fill::before { content: "\f294"; }
+.bi-cloud-arrow-down::before { content: "\f295"; }
+.bi-cloud-arrow-up-fill::before { content: "\f296"; }
+.bi-cloud-arrow-up::before { content: "\f297"; }
+.bi-cloud-check-fill::before { content: "\f298"; }
+.bi-cloud-check::before { content: "\f299"; }
+.bi-cloud-download-fill::before { content: "\f29a"; }
+.bi-cloud-download::before { content: "\f29b"; }
+.bi-cloud-drizzle-fill::before { content: "\f29c"; }
+.bi-cloud-drizzle::before { content: "\f29d"; }
+.bi-cloud-fill::before { content: "\f29e"; }
+.bi-cloud-fog-fill::before { content: "\f29f"; }
+.bi-cloud-fog::before { content: "\f2a0"; }
+.bi-cloud-fog2-fill::before { content: "\f2a1"; }
+.bi-cloud-fog2::before { content: "\f2a2"; }
+.bi-cloud-hail-fill::before { content: "\f2a3"; }
+.bi-cloud-hail::before { content: "\f2a4"; }
+.bi-cloud-haze-fill::before { content: "\f2a6"; }
+.bi-cloud-haze::before { content: "\f2a7"; }
+.bi-cloud-haze2-fill::before { content: "\f2a8"; }
+.bi-cloud-lightning-fill::before { content: "\f2a9"; }
+.bi-cloud-lightning-rain-fill::before { content: "\f2aa"; }
+.bi-cloud-lightning-rain::before { content: "\f2ab"; }
+.bi-cloud-lightning::before { content: "\f2ac"; }
+.bi-cloud-minus-fill::before { content: "\f2ad"; }
+.bi-cloud-minus::before { content: "\f2ae"; }
+.bi-cloud-moon-fill::before { content: "\f2af"; }
+.bi-cloud-moon::before { content: "\f2b0"; }
+.bi-cloud-plus-fill::before { content: "\f2b1"; }
+.bi-cloud-plus::before { content: "\f2b2"; }
+.bi-cloud-rain-fill::before { content: "\f2b3"; }
+.bi-cloud-rain-heavy-fill::before { content: "\f2b4"; }
+.bi-cloud-rain-heavy::before { content: "\f2b5"; }
+.bi-cloud-rain::before { content: "\f2b6"; }
+.bi-cloud-slash-fill::before { content: "\f2b7"; }
+.bi-cloud-slash::before { content: "\f2b8"; }
+.bi-cloud-sleet-fill::before { content: "\f2b9"; }
+.bi-cloud-sleet::before { content: "\f2ba"; }
+.bi-cloud-snow-fill::before { content: "\f2bb"; }
+.bi-cloud-snow::before { content: "\f2bc"; }
+.bi-cloud-sun-fill::before { content: "\f2bd"; }
+.bi-cloud-sun::before { content: "\f2be"; }
+.bi-cloud-upload-fill::before { content: "\f2bf"; }
+.bi-cloud-upload::before { content: "\f2c0"; }
+.bi-cloud::before { content: "\f2c1"; }
+.bi-clouds-fill::before { content: "\f2c2"; }
+.bi-clouds::before { content: "\f2c3"; }
+.bi-cloudy-fill::before { content: "\f2c4"; }
+.bi-cloudy::before { content: "\f2c5"; }
+.bi-code-slash::before { content: "\f2c6"; }
+.bi-code-square::before { content: "\f2c7"; }
+.bi-code::before { content: "\f2c8"; }
+.bi-collection-fill::before { content: "\f2c9"; }
+.bi-collection-play-fill::before { content: "\f2ca"; }
+.bi-collection-play::before { content: "\f2cb"; }
+.bi-collection::before { content: "\f2cc"; }
+.bi-columns-gap::before { content: "\f2cd"; }
+.bi-columns::before { content: "\f2ce"; }
+.bi-command::before { content: "\f2cf"; }
+.bi-compass-fill::before { content: "\f2d0"; }
+.bi-compass::before { content: "\f2d1"; }
+.bi-cone-striped::before { content: "\f2d2"; }
+.bi-cone::before { content: "\f2d3"; }
+.bi-controller::before { content: "\f2d4"; }
+.bi-cpu-fill::before { content: "\f2d5"; }
+.bi-cpu::before { content: "\f2d6"; }
+.bi-credit-card-2-back-fill::before { content: "\f2d7"; }
+.bi-credit-card-2-back::before { content: "\f2d8"; }
+.bi-credit-card-2-front-fill::before { content: "\f2d9"; }
+.bi-credit-card-2-front::before { content: "\f2da"; }
+.bi-credit-card-fill::before { content: "\f2db"; }
+.bi-credit-card::before { content: "\f2dc"; }
+.bi-crop::before { content: "\f2dd"; }
+.bi-cup-fill::before { content: "\f2de"; }
+.bi-cup-straw::before { content: "\f2df"; }
+.bi-cup::before { content: "\f2e0"; }
+.bi-cursor-fill::before { content: "\f2e1"; }
+.bi-cursor-text::before { content: "\f2e2"; }
+.bi-cursor::before { content: "\f2e3"; }
+.bi-dash-circle-dotted::before { content: "\f2e4"; }
+.bi-dash-circle-fill::before { content: "\f2e5"; }
+.bi-dash-circle::before { content: "\f2e6"; }
+.bi-dash-square-dotted::before { content: "\f2e7"; }
+.bi-dash-square-fill::before { content: "\f2e8"; }
+.bi-dash-square::before { content: "\f2e9"; }
+.bi-dash::before { content: "\f2ea"; }
+.bi-diagram-2-fill::before { content: "\f2eb"; }
+.bi-diagram-2::before { content: "\f2ec"; }
+.bi-diagram-3-fill::before { content: "\f2ed"; }
+.bi-diagram-3::before { content: "\f2ee"; }
+.bi-diamond-fill::before { content: "\f2ef"; }
+.bi-diamond-half::before { content: "\f2f0"; }
+.bi-diamond::before { content: "\f2f1"; }
+.bi-dice-1-fill::before { content: "\f2f2"; }
+.bi-dice-1::before { content: "\f2f3"; }
+.bi-dice-2-fill::before { content: "\f2f4"; }
+.bi-dice-2::before { content: "\f2f5"; }
+.bi-dice-3-fill::before { content: "\f2f6"; }
+.bi-dice-3::before { content: "\f2f7"; }
+.bi-dice-4-fill::before { content: "\f2f8"; }
+.bi-dice-4::before { content: "\f2f9"; }
+.bi-dice-5-fill::before { content: "\f2fa"; }
+.bi-dice-5::before { content: "\f2fb"; }
+.bi-dice-6-fill::before { content: "\f2fc"; }
+.bi-dice-6::before { content: "\f2fd"; }
+.bi-disc-fill::before { content: "\f2fe"; }
+.bi-disc::before { content: "\f2ff"; }
+.bi-discord::before { content: "\f300"; }
+.bi-display-fill::before { content: "\f301"; }
+.bi-display::before { content: "\f302"; }
+.bi-distribute-horizontal::before { content: "\f303"; }
+.bi-distribute-vertical::before { content: "\f304"; }
+.bi-door-closed-fill::before { content: "\f305"; }
+.bi-door-closed::before { content: "\f306"; }
+.bi-door-open-fill::before { content: "\f307"; }
+.bi-door-open::before { content: "\f308"; }
+.bi-dot::before { content: "\f309"; }
+.bi-download::before { content: "\f30a"; }
+.bi-droplet-fill::before { content: "\f30b"; }
+.bi-droplet-half::before { content: "\f30c"; }
+.bi-droplet::before { content: "\f30d"; }
+.bi-earbuds::before { content: "\f30e"; }
+.bi-easel-fill::before { content: "\f30f"; }
+.bi-easel::before { content: "\f310"; }
+.bi-egg-fill::before { content: "\f311"; }
+.bi-egg-fried::before { content: "\f312"; }
+.bi-egg::before { content: "\f313"; }
+.bi-eject-fill::before { content: "\f314"; }
+.bi-eject::before { content: "\f315"; }
+.bi-emoji-angry-fill::before { content: "\f316"; }
+.bi-emoji-angry::before { content: "\f317"; }
+.bi-emoji-dizzy-fill::before { content: "\f318"; }
+.bi-emoji-dizzy::before { content: "\f319"; }
+.bi-emoji-expressionless-fill::before { content: "\f31a"; }
+.bi-emoji-expressionless::before { content: "\f31b"; }
+.bi-emoji-frown-fill::before { content: "\f31c"; }
+.bi-emoji-frown::before { content: "\f31d"; }
+.bi-emoji-heart-eyes-fill::before { content: "\f31e"; }
+.bi-emoji-heart-eyes::before { content: "\f31f"; }
+.bi-emoji-laughing-fill::before { content: "\f320"; }
+.bi-emoji-laughing::before { content: "\f321"; }
+.bi-emoji-neutral-fill::before { content: "\f322"; }
+.bi-emoji-neutral::before { content: "\f323"; }
+.bi-emoji-smile-fill::before { content: "\f324"; }
+.bi-emoji-smile-upside-down-fill::before { content: "\f325"; }
+.bi-emoji-smile-upside-down::before { content: "\f326"; }
+.bi-emoji-smile::before { content: "\f327"; }
+.bi-emoji-sunglasses-fill::before { content: "\f328"; }
+.bi-emoji-sunglasses::before { content: "\f329"; }
+.bi-emoji-wink-fill::before { content: "\f32a"; }
+.bi-emoji-wink::before { content: "\f32b"; }
+.bi-envelope-fill::before { content: "\f32c"; }
+.bi-envelope-open-fill::before { content: "\f32d"; }
+.bi-envelope-open::before { content: "\f32e"; }
+.bi-envelope::before { content: "\f32f"; }
+.bi-eraser-fill::before { content: "\f330"; }
+.bi-eraser::before { content: "\f331"; }
+.bi-exclamation-circle-fill::before { content: "\f332"; }
+.bi-exclamation-circle::before { content: "\f333"; }
+.bi-exclamation-diamond-fill::before { content: "\f334"; }
+.bi-exclamation-diamond::before { content: "\f335"; }
+.bi-exclamation-octagon-fill::before { content: "\f336"; }
+.bi-exclamation-octagon::before { content: "\f337"; }
+.bi-exclamation-square-fill::before { content: "\f338"; }
+.bi-exclamation-square::before { content: "\f339"; }
+.bi-exclamation-triangle-fill::before { content: "\f33a"; }
+.bi-exclamation-triangle::before { content: "\f33b"; }
+.bi-exclamation::before { content: "\f33c"; }
+.bi-exclude::before { content: "\f33d"; }
+.bi-eye-fill::before { content: "\f33e"; }
+.bi-eye-slash-fill::before { content: "\f33f"; }
+.bi-eye-slash::before { content: "\f340"; }
+.bi-eye::before { content: "\f341"; }
+.bi-eyedropper::before { content: "\f342"; }
+.bi-eyeglasses::before { content: "\f343"; }
+.bi-facebook::before { content: "\f344"; }
+.bi-file-arrow-down-fill::before { content: "\f345"; }
+.bi-file-arrow-down::before { content: "\f346"; }
+.bi-file-arrow-up-fill::before { content: "\f347"; }
+.bi-file-arrow-up::before { content: "\f348"; }
+.bi-file-bar-graph-fill::before { content: "\f349"; }
+.bi-file-bar-graph::before { content: "\f34a"; }
+.bi-file-binary-fill::before { content: "\f34b"; }
+.bi-file-binary::before { content: "\f34c"; }
+.bi-file-break-fill::before { content: "\f34d"; }
+.bi-file-break::before { content: "\f34e"; }
+.bi-file-check-fill::before { content: "\f34f"; }
+.bi-file-check::before { content: "\f350"; }
+.bi-file-code-fill::before { content: "\f351"; }
+.bi-file-code::before { content: "\f352"; }
+.bi-file-diff-fill::before { content: "\f353"; }
+.bi-file-diff::before { content: "\f354"; }
+.bi-file-earmark-arrow-down-fill::before { content: "\f355"; }
+.bi-file-earmark-arrow-down::before { content: "\f356"; }
+.bi-file-earmark-arrow-up-fill::before { content: "\f357"; }
+.bi-file-earmark-arrow-up::before { content: "\f358"; }
+.bi-file-earmark-bar-graph-fill::before { content: "\f359"; }
+.bi-file-earmark-bar-graph::before { content: "\f35a"; }
+.bi-file-earmark-binary-fill::before { content: "\f35b"; }
+.bi-file-earmark-binary::before { content: "\f35c"; }
+.bi-file-earmark-break-fill::before { content: "\f35d"; }
+.bi-file-earmark-break::before { content: "\f35e"; }
+.bi-file-earmark-check-fill::before { content: "\f35f"; }
+.bi-file-earmark-check::before { content: "\f360"; }
+.bi-file-earmark-code-fill::before { content: "\f361"; }
+.bi-file-earmark-code::before { content: "\f362"; }
+.bi-file-earmark-diff-fill::before { content: "\f363"; }
+.bi-file-earmark-diff::before { content: "\f364"; }
+.bi-file-earmark-easel-fill::before { content: "\f365"; }
+.bi-file-earmark-easel::before { content: "\f366"; }
+.bi-file-earmark-excel-fill::before { content: "\f367"; }
+.bi-file-earmark-excel::before { content: "\f368"; }
+.bi-file-earmark-fill::before { content: "\f369"; }
+.bi-file-earmark-font-fill::before { content: "\f36a"; }
+.bi-file-earmark-font::before { content: "\f36b"; }
+.bi-file-earmark-image-fill::before { content: "\f36c"; }
+.bi-file-earmark-image::before { content: "\f36d"; }
+.bi-file-earmark-lock-fill::before { content: "\f36e"; }
+.bi-file-earmark-lock::before { content: "\f36f"; }
+.bi-file-earmark-lock2-fill::before { content: "\f370"; }
+.bi-file-earmark-lock2::before { content: "\f371"; }
+.bi-file-earmark-medical-fill::before { content: "\f372"; }
+.bi-file-earmark-medical::before { content: "\f373"; }
+.bi-file-earmark-minus-fill::before { content: "\f374"; }
+.bi-file-earmark-minus::before { content: "\f375"; }
+.bi-file-earmark-music-fill::before { content: "\f376"; }
+.bi-file-earmark-music::before { content: "\f377"; }
+.bi-file-earmark-person-fill::before { content: "\f378"; }
+.bi-file-earmark-person::before { content: "\f379"; }
+.bi-file-earmark-play-fill::before { content: "\f37a"; }
+.bi-file-earmark-play::before { content: "\f37b"; }
+.bi-file-earmark-plus-fill::before { content: "\f37c"; }
+.bi-file-earmark-plus::before { content: "\f37d"; }
+.bi-file-earmark-post-fill::before { content: "\f37e"; }
+.bi-file-earmark-post::before { content: "\f37f"; }
+.bi-file-earmark-ppt-fill::before { content: "\f380"; }
+.bi-file-earmark-ppt::before { content: "\f381"; }
+.bi-file-earmark-richtext-fill::before { content: "\f382"; }
+.bi-file-earmark-richtext::before { content: "\f383"; }
+.bi-file-earmark-ruled-fill::before { content: "\f384"; }
+.bi-file-earmark-ruled::before { content: "\f385"; }
+.bi-file-earmark-slides-fill::before { content: "\f386"; }
+.bi-file-earmark-slides::before { content: "\f387"; }
+.bi-file-earmark-spreadsheet-fill::before { content: "\f388"; }
+.bi-file-earmark-spreadsheet::before { content: "\f389"; }
+.bi-file-earmark-text-fill::before { content: "\f38a"; }
+.bi-file-earmark-text::before { content: "\f38b"; }
+.bi-file-earmark-word-fill::before { content: "\f38c"; }
+.bi-file-earmark-word::before { content: "\f38d"; }
+.bi-file-earmark-x-fill::before { content: "\f38e"; }
+.bi-file-earmark-x::before { content: "\f38f"; }
+.bi-file-earmark-zip-fill::before { content: "\f390"; }
+.bi-file-earmark-zip::before { content: "\f391"; }
+.bi-file-earmark::before { content: "\f392"; }
+.bi-file-easel-fill::before { content: "\f393"; }
+.bi-file-easel::before { content: "\f394"; }
+.bi-file-excel-fill::before { content: "\f395"; }
+.bi-file-excel::before { content: "\f396"; }
+.bi-file-fill::before { content: "\f397"; }
+.bi-file-font-fill::before { content: "\f398"; }
+.bi-file-font::before { content: "\f399"; }
+.bi-file-image-fill::before { content: "\f39a"; }
+.bi-file-image::before { content: "\f39b"; }
+.bi-file-lock-fill::before { content: "\f39c"; }
+.bi-file-lock::before { content: "\f39d"; }
+.bi-file-lock2-fill::before { content: "\f39e"; }
+.bi-file-lock2::before { content: "\f39f"; }
+.bi-file-medical-fill::before { content: "\f3a0"; }
+.bi-file-medical::before { content: "\f3a1"; }
+.bi-file-minus-fill::before { content: "\f3a2"; }
+.bi-file-minus::before { content: "\f3a3"; }
+.bi-file-music-fill::before { content: "\f3a4"; }
+.bi-file-music::before { content: "\f3a5"; }
+.bi-file-person-fill::before { content: "\f3a6"; }
+.bi-file-person::before { content: "\f3a7"; }
+.bi-file-play-fill::before { content: "\f3a8"; }
+.bi-file-play::before { content: "\f3a9"; }
+.bi-file-plus-fill::before { content: "\f3aa"; }
+.bi-file-plus::before { content: "\f3ab"; }
+.bi-file-post-fill::before { content: "\f3ac"; }
+.bi-file-post::before { content: "\f3ad"; }
+.bi-file-ppt-fill::before { content: "\f3ae"; }
+.bi-file-ppt::before { content: "\f3af"; }
+.bi-file-richtext-fill::before { content: "\f3b0"; }
+.bi-file-richtext::before { content: "\f3b1"; }
+.bi-file-ruled-fill::before { content: "\f3b2"; }
+.bi-file-ruled::before { content: "\f3b3"; }
+.bi-file-slides-fill::before { content: "\f3b4"; }
+.bi-file-slides::before { content: "\f3b5"; }
+.bi-file-spreadsheet-fill::before { content: "\f3b6"; }
+.bi-file-spreadsheet::before { content: "\f3b7"; }
+.bi-file-text-fill::before { content: "\f3b8"; }
+.bi-file-text::before { content: "\f3b9"; }
+.bi-file-word-fill::before { content: "\f3ba"; }
+.bi-file-word::before { content: "\f3bb"; }
+.bi-file-x-fill::before { content: "\f3bc"; }
+.bi-file-x::before { content: "\f3bd"; }
+.bi-file-zip-fill::before { content: "\f3be"; }
+.bi-file-zip::before { content: "\f3bf"; }
+.bi-file::before { content: "\f3c0"; }
+.bi-files-alt::before { content: "\f3c1"; }
+.bi-files::before { content: "\f3c2"; }
+.bi-film::before { content: "\f3c3"; }
+.bi-filter-circle-fill::before { content: "\f3c4"; }
+.bi-filter-circle::before { content: "\f3c5"; }
+.bi-filter-left::before { content: "\f3c6"; }
+.bi-filter-right::before { content: "\f3c7"; }
+.bi-filter-square-fill::before { content: "\f3c8"; }
+.bi-filter-square::before { content: "\f3c9"; }
+.bi-filter::before { content: "\f3ca"; }
+.bi-flag-fill::before { content: "\f3cb"; }
+.bi-flag::before { content: "\f3cc"; }
+.bi-flower1::before { content: "\f3cd"; }
+.bi-flower2::before { content: "\f3ce"; }
+.bi-flower3::before { content: "\f3cf"; }
+.bi-folder-check::before { content: "\f3d0"; }
+.bi-folder-fill::before { content: "\f3d1"; }
+.bi-folder-minus::before { content: "\f3d2"; }
+.bi-folder-plus::before { content: "\f3d3"; }
+.bi-folder-symlink-fill::before { content: "\f3d4"; }
+.bi-folder-symlink::before { content: "\f3d5"; }
+.bi-folder-x::before { content: "\f3d6"; }
+.bi-folder::before { content: "\f3d7"; }
+.bi-folder2-open::before { content: "\f3d8"; }
+.bi-folder2::before { content: "\f3d9"; }
+.bi-fonts::before { content: "\f3da"; }
+.bi-forward-fill::before { content: "\f3db"; }
+.bi-forward::before { content: "\f3dc"; }
+.bi-front::before { content: "\f3dd"; }
+.bi-fullscreen-exit::before { content: "\f3de"; }
+.bi-fullscreen::before { content: "\f3df"; }
+.bi-funnel-fill::before { content: "\f3e0"; }
+.bi-funnel::before { content: "\f3e1"; }
+.bi-gear-fill::before { content: "\f3e2"; }
+.bi-gear-wide-connected::before { content: "\f3e3"; }
+.bi-gear-wide::before { content: "\f3e4"; }
+.bi-gear::before { content: "\f3e5"; }
+.bi-gem::before { content: "\f3e6"; }
+.bi-geo-alt-fill::before { content: "\f3e7"; }
+.bi-geo-alt::before { content: "\f3e8"; }
+.bi-geo-fill::before { content: "\f3e9"; }
+.bi-geo::before { content: "\f3ea"; }
+.bi-gift-fill::before { content: "\f3eb"; }
+.bi-gift::before { content: "\f3ec"; }
+.bi-github::before { content: "\f3ed"; }
+.bi-globe::before { content: "\f3ee"; }
+.bi-globe2::before { content: "\f3ef"; }
+.bi-google::before { content: "\f3f0"; }
+.bi-graph-down::before { content: "\f3f1"; }
+.bi-graph-up::before { content: "\f3f2"; }
+.bi-grid-1x2-fill::before { content: "\f3f3"; }
+.bi-grid-1x2::before { content: "\f3f4"; }
+.bi-grid-3x2-gap-fill::before { content: "\f3f5"; }
+.bi-grid-3x2-gap::before { content: "\f3f6"; }
+.bi-grid-3x2::before { content: "\f3f7"; }
+.bi-grid-3x3-gap-fill::before { content: "\f3f8"; }
+.bi-grid-3x3-gap::before { content: "\f3f9"; }
+.bi-grid-3x3::before { content: "\f3fa"; }
+.bi-grid-fill::before { content: "\f3fb"; }
+.bi-grid::before { content: "\f3fc"; }
+.bi-grip-horizontal::before { content: "\f3fd"; }
+.bi-grip-vertical::before { content: "\f3fe"; }
+.bi-hammer::before { content: "\f3ff"; }
+.bi-hand-index-fill::before { content: "\f400"; }
+.bi-hand-index-thumb-fill::before { content: "\f401"; }
+.bi-hand-index-thumb::before { content: "\f402"; }
+.bi-hand-index::before { content: "\f403"; }
+.bi-hand-thumbs-down-fill::before { content: "\f404"; }
+.bi-hand-thumbs-down::before { content: "\f405"; }
+.bi-hand-thumbs-up-fill::before { content: "\f406"; }
+.bi-hand-thumbs-up::before { content: "\f407"; }
+.bi-handbag-fill::before { content: "\f408"; }
+.bi-handbag::before { content: "\f409"; }
+.bi-hash::before { content: "\f40a"; }
+.bi-hdd-fill::before { content: "\f40b"; }
+.bi-hdd-network-fill::before { content: "\f40c"; }
+.bi-hdd-network::before { content: "\f40d"; }
+.bi-hdd-rack-fill::before { content: "\f40e"; }
+.bi-hdd-rack::before { content: "\f40f"; }
+.bi-hdd-stack-fill::before { content: "\f410"; }
+.bi-hdd-stack::before { content: "\f411"; }
+.bi-hdd::before { content: "\f412"; }
+.bi-headphones::before { content: "\f413"; }
+.bi-headset::before { content: "\f414"; }
+.bi-heart-fill::before { content: "\f415"; }
+.bi-heart-half::before { content: "\f416"; }
+.bi-heart::before { content: "\f417"; }
+.bi-heptagon-fill::before { content: "\f418"; }
+.bi-heptagon-half::before { content: "\f419"; }
+.bi-heptagon::before { content: "\f41a"; }
+.bi-hexagon-fill::before { content: "\f41b"; }
+.bi-hexagon-half::before { content: "\f41c"; }
+.bi-hexagon::before { content: "\f41d"; }
+.bi-hourglass-bottom::before { content: "\f41e"; }
+.bi-hourglass-split::before { content: "\f41f"; }
+.bi-hourglass-top::before { content: "\f420"; }
+.bi-hourglass::before { content: "\f421"; }
+.bi-house-door-fill::before { content: "\f422"; }
+.bi-house-door::before { content: "\f423"; }
+.bi-house-fill::before { content: "\f424"; }
+.bi-house::before { content: "\f425"; }
+.bi-hr::before { content: "\f426"; }
+.bi-hurricane::before { content: "\f427"; }
+.bi-image-alt::before { content: "\f428"; }
+.bi-image-fill::before { content: "\f429"; }
+.bi-image::before { content: "\f42a"; }
+.bi-images::before { content: "\f42b"; }
+.bi-inbox-fill::before { content: "\f42c"; }
+.bi-inbox::before { content: "\f42d"; }
+.bi-inboxes-fill::before { content: "\f42e"; }
+.bi-inboxes::before { content: "\f42f"; }
+.bi-info-circle-fill::before { content: "\f430"; }
+.bi-info-circle::before { content: "\f431"; }
+.bi-info-square-fill::before { content: "\f432"; }
+.bi-info-square::before { content: "\f433"; }
+.bi-info::before { content: "\f434"; }
+.bi-input-cursor-text::before { content: "\f435"; }
+.bi-input-cursor::before { content: "\f436"; }
+.bi-instagram::before { content: "\f437"; }
+.bi-intersect::before { content: "\f438"; }
+.bi-journal-album::before { content: "\f439"; }
+.bi-journal-arrow-down::before { content: "\f43a"; }
+.bi-journal-arrow-up::before { content: "\f43b"; }
+.bi-journal-bookmark-fill::before { content: "\f43c"; }
+.bi-journal-bookmark::before { content: "\f43d"; }
+.bi-journal-check::before { content: "\f43e"; }
+.bi-journal-code::before { content: "\f43f"; }
+.bi-journal-medical::before { content: "\f440"; }
+.bi-journal-minus::before { content: "\f441"; }
+.bi-journal-plus::before { content: "\f442"; }
+.bi-journal-richtext::before { content: "\f443"; }
+.bi-journal-text::before { content: "\f444"; }
+.bi-journal-x::before { content: "\f445"; }
+.bi-journal::before { content: "\f446"; }
+.bi-journals::before { content: "\f447"; }
+.bi-joystick::before { content: "\f448"; }
+.bi-justify-left::before { content: "\f449"; }
+.bi-justify-right::before { content: "\f44a"; }
+.bi-justify::before { content: "\f44b"; }
+.bi-kanban-fill::before { content: "\f44c"; }
+.bi-kanban::before { content: "\f44d"; }
+.bi-key-fill::before { content: "\f44e"; }
+.bi-key::before { content: "\f44f"; }
+.bi-keyboard-fill::before { content: "\f450"; }
+.bi-keyboard::before { content: "\f451"; }
+.bi-ladder::before { content: "\f452"; }
+.bi-lamp-fill::before { content: "\f453"; }
+.bi-lamp::before { content: "\f454"; }
+.bi-laptop-fill::before { content: "\f455"; }
+.bi-laptop::before { content: "\f456"; }
+.bi-layer-backward::before { content: "\f457"; }
+.bi-layer-forward::before { content: "\f458"; }
+.bi-layers-fill::before { content: "\f459"; }
+.bi-layers-half::before { content: "\f45a"; }
+.bi-layers::before { content: "\f45b"; }
+.bi-layout-sidebar-inset-reverse::before { content: "\f45c"; }
+.bi-layout-sidebar-inset::before { content: "\f45d"; }
+.bi-layout-sidebar-reverse::before { content: "\f45e"; }
+.bi-layout-sidebar::before { content: "\f45f"; }
+.bi-layout-split::before { content: "\f460"; }
+.bi-layout-text-sidebar-reverse::before { content: "\f461"; }
+.bi-layout-text-sidebar::before { content: "\f462"; }
+.bi-layout-text-window-reverse::before { content: "\f463"; }
+.bi-layout-text-window::before { content: "\f464"; }
+.bi-layout-three-columns::before { content: "\f465"; }
+.bi-layout-wtf::before { content: "\f466"; }
+.bi-life-preserver::before { content: "\f467"; }
+.bi-lightbulb-fill::before { content: "\f468"; }
+.bi-lightbulb-off-fill::before { content: "\f469"; }
+.bi-lightbulb-off::before { content: "\f46a"; }
+.bi-lightbulb::before { content: "\f46b"; }
+.bi-lightning-charge-fill::before { content: "\f46c"; }
+.bi-lightning-charge::before { content: "\f46d"; }
+.bi-lightning-fill::before { content: "\f46e"; }
+.bi-lightning::before { content: "\f46f"; }
+.bi-link-45deg::before { content: "\f470"; }
+.bi-link::before { content: "\f471"; }
+.bi-linkedin::before { content: "\f472"; }
+.bi-list-check::before { content: "\f473"; }
+.bi-list-nested::before { content: "\f474"; }
+.bi-list-ol::before { content: "\f475"; }
+.bi-list-stars::before { content: "\f476"; }
+.bi-list-task::before { content: "\f477"; }
+.bi-list-ul::before { content: "\f478"; }
+.bi-list::before { content: "\f479"; }
+.bi-lock-fill::before { content: "\f47a"; }
+.bi-lock::before { content: "\f47b"; }
+.bi-mailbox::before { content: "\f47c"; }
+.bi-mailbox2::before { content: "\f47d"; }
+.bi-map-fill::before { content: "\f47e"; }
+.bi-map::before { content: "\f47f"; }
+.bi-markdown-fill::before { content: "\f480"; }
+.bi-markdown::before { content: "\f481"; }
+.bi-mask::before { content: "\f482"; }
+.bi-megaphone-fill::before { content: "\f483"; }
+.bi-megaphone::before { content: "\f484"; }
+.bi-menu-app-fill::before { content: "\f485"; }
+.bi-menu-app::before { content: "\f486"; }
+.bi-menu-button-fill::before { content: "\f487"; }
+.bi-menu-button-wide-fill::before { content: "\f488"; }
+.bi-menu-button-wide::before { content: "\f489"; }
+.bi-menu-button::before { content: "\f48a"; }
+.bi-menu-down::before { content: "\f48b"; }
+.bi-menu-up::before { content: "\f48c"; }
+.bi-mic-fill::before { content: "\f48d"; }
+.bi-mic-mute-fill::before { content: "\f48e"; }
+.bi-mic-mute::before { content: "\f48f"; }
+.bi-mic::before { content: "\f490"; }
+.bi-minecart-loaded::before { content: "\f491"; }
+.bi-minecart::before { content: "\f492"; }
+.bi-moisture::before { content: "\f493"; }
+.bi-moon-fill::before { content: "\f494"; }
+.bi-moon-stars-fill::before { content: "\f495"; }
+.bi-moon-stars::before { content: "\f496"; }
+.bi-moon::before { content: "\f497"; }
+.bi-mouse-fill::before { content: "\f498"; }
+.bi-mouse::before { content: "\f499"; }
+.bi-mouse2-fill::before { content: "\f49a"; }
+.bi-mouse2::before { content: "\f49b"; }
+.bi-mouse3-fill::before { content: "\f49c"; }
+.bi-mouse3::before { content: "\f49d"; }
+.bi-music-note-beamed::before { content: "\f49e"; }
+.bi-music-note-list::before { content: "\f49f"; }
+.bi-music-note::before { content: "\f4a0"; }
+.bi-music-player-fill::before { content: "\f4a1"; }
+.bi-music-player::before { content: "\f4a2"; }
+.bi-newspaper::before { content: "\f4a3"; }
+.bi-node-minus-fill::before { content: "\f4a4"; }
+.bi-node-minus::before { content: "\f4a5"; }
+.bi-node-plus-fill::before { content: "\f4a6"; }
+.bi-node-plus::before { content: "\f4a7"; }
+.bi-nut-fill::before { content: "\f4a8"; }
+.bi-nut::before { content: "\f4a9"; }
+.bi-octagon-fill::before { content: "\f4aa"; }
+.bi-octagon-half::before { content: "\f4ab"; }
+.bi-octagon::before { content: "\f4ac"; }
+.bi-option::before { content: "\f4ad"; }
+.bi-outlet::before { content: "\f4ae"; }
+.bi-paint-bucket::before { content: "\f4af"; }
+.bi-palette-fill::before { content: "\f4b0"; }
+.bi-palette::before { content: "\f4b1"; }
+.bi-palette2::before { content: "\f4b2"; }
+.bi-paperclip::before { content: "\f4b3"; }
+.bi-paragraph::before { content: "\f4b4"; }
+.bi-patch-check-fill::before { content: "\f4b5"; }
+.bi-patch-check::before { content: "\f4b6"; }
+.bi-patch-exclamation-fill::before { content: "\f4b7"; }
+.bi-patch-exclamation::before { content: "\f4b8"; }
+.bi-patch-minus-fill::before { content: "\f4b9"; }
+.bi-patch-minus::before { content: "\f4ba"; }
+.bi-patch-plus-fill::before { content: "\f4bb"; }
+.bi-patch-plus::before { content: "\f4bc"; }
+.bi-patch-question-fill::before { content: "\f4bd"; }
+.bi-patch-question::before { content: "\f4be"; }
+.bi-pause-btn-fill::before { content: "\f4bf"; }
+.bi-pause-btn::before { content: "\f4c0"; }
+.bi-pause-circle-fill::before { content: "\f4c1"; }
+.bi-pause-circle::before { content: "\f4c2"; }
+.bi-pause-fill::before { content: "\f4c3"; }
+.bi-pause::before { content: "\f4c4"; }
+.bi-peace-fill::before { content: "\f4c5"; }
+.bi-peace::before { content: "\f4c6"; }
+.bi-pen-fill::before { content: "\f4c7"; }
+.bi-pen::before { content: "\f4c8"; }
+.bi-pencil-fill::before { content: "\f4c9"; }
+.bi-pencil-square::before { content: "\f4ca"; }
+.bi-pencil::before { content: "\f4cb"; }
+.bi-pentagon-fill::before { content: "\f4cc"; }
+.bi-pentagon-half::before { content: "\f4cd"; }
+.bi-pentagon::before { content: "\f4ce"; }
+.bi-people-fill::before { content: "\f4cf"; }
+.bi-people::before { content: "\f4d0"; }
+.bi-percent::before { content: "\f4d1"; }
+.bi-person-badge-fill::before { content: "\f4d2"; }
+.bi-person-badge::before { content: "\f4d3"; }
+.bi-person-bounding-box::before { content: "\f4d4"; }
+.bi-person-check-fill::before { content: "\f4d5"; }
+.bi-person-check::before { content: "\f4d6"; }
+.bi-person-circle::before { content: "\f4d7"; }
+.bi-person-dash-fill::before { content: "\f4d8"; }
+.bi-person-dash::before { content: "\f4d9"; }
+.bi-person-fill::before { content: "\f4da"; }
+.bi-person-lines-fill::before { content: "\f4db"; }
+.bi-person-plus-fill::before { content: "\f4dc"; }
+.bi-person-plus::before { content: "\f4dd"; }
+.bi-person-square::before { content: "\f4de"; }
+.bi-person-x-fill::before { content: "\f4df"; }
+.bi-person-x::before { content: "\f4e0"; }
+.bi-person::before { content: "\f4e1"; }
+.bi-phone-fill::before { content: "\f4e2"; }
+.bi-phone-landscape-fill::before { content: "\f4e3"; }
+.bi-phone-landscape::before { content: "\f4e4"; }
+.bi-phone-vibrate-fill::before { content: "\f4e5"; }
+.bi-phone-vibrate::before { content: "\f4e6"; }
+.bi-phone::before { content: "\f4e7"; }
+.bi-pie-chart-fill::before { content: "\f4e8"; }
+.bi-pie-chart::before { content: "\f4e9"; }
+.bi-pin-angle-fill::before { content: "\f4ea"; }
+.bi-pin-angle::before { content: "\f4eb"; }
+.bi-pin-fill::before { content: "\f4ec"; }
+.bi-pin::before { content: "\f4ed"; }
+.bi-pip-fill::before { content: "\f4ee"; }
+.bi-pip::before { content: "\f4ef"; }
+.bi-play-btn-fill::before { content: "\f4f0"; }
+.bi-play-btn::before { content: "\f4f1"; }
+.bi-play-circle-fill::before { content: "\f4f2"; }
+.bi-play-circle::before { content: "\f4f3"; }
+.bi-play-fill::before { content: "\f4f4"; }
+.bi-play::before { content: "\f4f5"; }
+.bi-plug-fill::before { content: "\f4f6"; }
+.bi-plug::before { content: "\f4f7"; }
+.bi-plus-circle-dotted::before { content: "\f4f8"; }
+.bi-plus-circle-fill::before { content: "\f4f9"; }
+.bi-plus-circle::before { content: "\f4fa"; }
+.bi-plus-square-dotted::before { content: "\f4fb"; }
+.bi-plus-square-fill::before { content: "\f4fc"; }
+.bi-plus-square::before { content: "\f4fd"; }
+.bi-plus::before { content: "\f4fe"; }
+.bi-power::before { content: "\f4ff"; }
+.bi-printer-fill::before { content: "\f500"; }
+.bi-printer::before { content: "\f501"; }
+.bi-puzzle-fill::before { content: "\f502"; }
+.bi-puzzle::before { content: "\f503"; }
+.bi-question-circle-fill::before { content: "\f504"; }
+.bi-question-circle::before { content: "\f505"; }
+.bi-question-diamond-fill::before { content: "\f506"; }
+.bi-question-diamond::before { content: "\f507"; }
+.bi-question-octagon-fill::before { content: "\f508"; }
+.bi-question-octagon::before { content: "\f509"; }
+.bi-question-square-fill::before { content: "\f50a"; }
+.bi-question-square::before { content: "\f50b"; }
+.bi-question::before { content: "\f50c"; }
+.bi-rainbow::before { content: "\f50d"; }
+.bi-receipt-cutoff::before { content: "\f50e"; }
+.bi-receipt::before { content: "\f50f"; }
+.bi-reception-0::before { content: "\f510"; }
+.bi-reception-1::before { content: "\f511"; }
+.bi-reception-2::before { content: "\f512"; }
+.bi-reception-3::before { content: "\f513"; }
+.bi-reception-4::before { content: "\f514"; }
+.bi-record-btn-fill::before { content: "\f515"; }
+.bi-record-btn::before { content: "\f516"; }
+.bi-record-circle-fill::before { content: "\f517"; }
+.bi-record-circle::before { content: "\f518"; }
+.bi-record-fill::before { content: "\f519"; }
+.bi-record::before { content: "\f51a"; }
+.bi-record2-fill::before { content: "\f51b"; }
+.bi-record2::before { content: "\f51c"; }
+.bi-reply-all-fill::before { content: "\f51d"; }
+.bi-reply-all::before { content: "\f51e"; }
+.bi-reply-fill::before { content: "\f51f"; }
+.bi-reply::before { content: "\f520"; }
+.bi-rss-fill::before { content: "\f521"; }
+.bi-rss::before { content: "\f522"; }
+.bi-rulers::before { content: "\f523"; }
+.bi-save-fill::before { content: "\f524"; }
+.bi-save::before { content: "\f525"; }
+.bi-save2-fill::before { content: "\f526"; }
+.bi-save2::before { content: "\f527"; }
+.bi-scissors::before { content: "\f528"; }
+.bi-screwdriver::before { content: "\f529"; }
+.bi-search::before { content: "\f52a"; }
+.bi-segmented-nav::before { content: "\f52b"; }
+.bi-server::before { content: "\f52c"; }
+.bi-share-fill::before { content: "\f52d"; }
+.bi-share::before { content: "\f52e"; }
+.bi-shield-check::before { content: "\f52f"; }
+.bi-shield-exclamation::before { content: "\f530"; }
+.bi-shield-fill-check::before { content: "\f531"; }
+.bi-shield-fill-exclamation::before { content: "\f532"; }
+.bi-shield-fill-minus::before { content: "\f533"; }
+.bi-shield-fill-plus::before { content: "\f534"; }
+.bi-shield-fill-x::before { content: "\f535"; }
+.bi-shield-fill::before { content: "\f536"; }
+.bi-shield-lock-fill::before { content: "\f537"; }
+.bi-shield-lock::before { content: "\f538"; }
+.bi-shield-minus::before { content: "\f539"; }
+.bi-shield-plus::before { content: "\f53a"; }
+.bi-shield-shaded::before { content: "\f53b"; }
+.bi-shield-slash-fill::before { content: "\f53c"; }
+.bi-shield-slash::before { content: "\f53d"; }
+.bi-shield-x::before { content: "\f53e"; }
+.bi-shield::before { content: "\f53f"; }
+.bi-shift-fill::before { content: "\f540"; }
+.bi-shift::before { content: "\f541"; }
+.bi-shop-window::before { content: "\f542"; }
+.bi-shop::before { content: "\f543"; }
+.bi-shuffle::before { content: "\f544"; }
+.bi-signpost-2-fill::before { content: "\f545"; }
+.bi-signpost-2::before { content: "\f546"; }
+.bi-signpost-fill::before { content: "\f547"; }
+.bi-signpost-split-fill::before { content: "\f548"; }
+.bi-signpost-split::before { content: "\f549"; }
+.bi-signpost::before { content: "\f54a"; }
+.bi-sim-fill::before { content: "\f54b"; }
+.bi-sim::before { content: "\f54c"; }
+.bi-skip-backward-btn-fill::before { content: "\f54d"; }
+.bi-skip-backward-btn::before { content: "\f54e"; }
+.bi-skip-backward-circle-fill::before { content: "\f54f"; }
+.bi-skip-backward-circle::before { content: "\f550"; }
+.bi-skip-backward-fill::before { content: "\f551"; }
+.bi-skip-backward::before { content: "\f552"; }
+.bi-skip-end-btn-fill::before { content: "\f553"; }
+.bi-skip-end-btn::before { content: "\f554"; }
+.bi-skip-end-circle-fill::before { content: "\f555"; }
+.bi-skip-end-circle::before { content: "\f556"; }
+.bi-skip-end-fill::before { content: "\f557"; }
+.bi-skip-end::before { content: "\f558"; }
+.bi-skip-forward-btn-fill::before { content: "\f559"; }
+.bi-skip-forward-btn::before { content: "\f55a"; }
+.bi-skip-forward-circle-fill::before { content: "\f55b"; }
+.bi-skip-forward-circle::before { content: "\f55c"; }
+.bi-skip-forward-fill::before { content: "\f55d"; }
+.bi-skip-forward::before { content: "\f55e"; }
+.bi-skip-start-btn-fill::before { content: "\f55f"; }
+.bi-skip-start-btn::before { content: "\f560"; }
+.bi-skip-start-circle-fill::before { content: "\f561"; }
+.bi-skip-start-circle::before { content: "\f562"; }
+.bi-skip-start-fill::before { content: "\f563"; }
+.bi-skip-start::before { content: "\f564"; }
+.bi-slack::before { content: "\f565"; }
+.bi-slash-circle-fill::before { content: "\f566"; }
+.bi-slash-circle::before { content: "\f567"; }
+.bi-slash-square-fill::before { content: "\f568"; }
+.bi-slash-square::before { content: "\f569"; }
+.bi-slash::before { content: "\f56a"; }
+.bi-sliders::before { content: "\f56b"; }
+.bi-smartwatch::before { content: "\f56c"; }
+.bi-snow::before { content: "\f56d"; }
+.bi-snow2::before { content: "\f56e"; }
+.bi-snow3::before { content: "\f56f"; }
+.bi-sort-alpha-down-alt::before { content: "\f570"; }
+.bi-sort-alpha-down::before { content: "\f571"; }
+.bi-sort-alpha-up-alt::before { content: "\f572"; }
+.bi-sort-alpha-up::before { content: "\f573"; }
+.bi-sort-down-alt::before { content: "\f574"; }
+.bi-sort-down::before { content: "\f575"; }
+.bi-sort-numeric-down-alt::before { content: "\f576"; }
+.bi-sort-numeric-down::before { content: "\f577"; }
+.bi-sort-numeric-up-alt::before { content: "\f578"; }
+.bi-sort-numeric-up::before { content: "\f579"; }
+.bi-sort-up-alt::before { content: "\f57a"; }
+.bi-sort-up::before { content: "\f57b"; }
+.bi-soundwave::before { content: "\f57c"; }
+.bi-speaker-fill::before { content: "\f57d"; }
+.bi-speaker::before { content: "\f57e"; }
+.bi-speedometer::before { content: "\f57f"; }
+.bi-speedometer2::before { content: "\f580"; }
+.bi-spellcheck::before { content: "\f581"; }
+.bi-square-fill::before { content: "\f582"; }
+.bi-square-half::before { content: "\f583"; }
+.bi-square::before { content: "\f584"; }
+.bi-stack::before { content: "\f585"; }
+.bi-star-fill::before { content: "\f586"; }
+.bi-star-half::before { content: "\f587"; }
+.bi-star::before { content: "\f588"; }
+.bi-stars::before { content: "\f589"; }
+.bi-stickies-fill::before { content: "\f58a"; }
+.bi-stickies::before { content: "\f58b"; }
+.bi-sticky-fill::before { content: "\f58c"; }
+.bi-sticky::before { content: "\f58d"; }
+.bi-stop-btn-fill::before { content: "\f58e"; }
+.bi-stop-btn::before { content: "\f58f"; }
+.bi-stop-circle-fill::before { content: "\f590"; }
+.bi-stop-circle::before { content: "\f591"; }
+.bi-stop-fill::before { content: "\f592"; }
+.bi-stop::before { content: "\f593"; }
+.bi-stoplights-fill::before { content: "\f594"; }
+.bi-stoplights::before { content: "\f595"; }
+.bi-stopwatch-fill::before { content: "\f596"; }
+.bi-stopwatch::before { content: "\f597"; }
+.bi-subtract::before { content: "\f598"; }
+.bi-suit-club-fill::before { content: "\f599"; }
+.bi-suit-club::before { content: "\f59a"; }
+.bi-suit-diamond-fill::before { content: "\f59b"; }
+.bi-suit-diamond::before { content: "\f59c"; }
+.bi-suit-heart-fill::before { content: "\f59d"; }
+.bi-suit-heart::before { content: "\f59e"; }
+.bi-suit-spade-fill::before { content: "\f59f"; }
+.bi-suit-spade::before { content: "\f5a0"; }
+.bi-sun-fill::before { content: "\f5a1"; }
+.bi-sun::before { content: "\f5a2"; }
+.bi-sunglasses::before { content: "\f5a3"; }
+.bi-sunrise-fill::before { content: "\f5a4"; }
+.bi-sunrise::before { content: "\f5a5"; }
+.bi-sunset-fill::before { content: "\f5a6"; }
+.bi-sunset::before { content: "\f5a7"; }
+.bi-symmetry-horizontal::before { content: "\f5a8"; }
+.bi-symmetry-vertical::before { content: "\f5a9"; }
+.bi-table::before { content: "\f5aa"; }
+.bi-tablet-fill::before { content: "\f5ab"; }
+.bi-tablet-landscape-fill::before { content: "\f5ac"; }
+.bi-tablet-landscape::before { content: "\f5ad"; }
+.bi-tablet::before { content: "\f5ae"; }
+.bi-tag-fill::before { content: "\f5af"; }
+.bi-tag::before { content: "\f5b0"; }
+.bi-tags-fill::before { content: "\f5b1"; }
+.bi-tags::before { content: "\f5b2"; }
+.bi-telegram::before { content: "\f5b3"; }
+.bi-telephone-fill::before { content: "\f5b4"; }
+.bi-telephone-forward-fill::before { content: "\f5b5"; }
+.bi-telephone-forward::before { content: "\f5b6"; }
+.bi-telephone-inbound-fill::before { content: "\f5b7"; }
+.bi-telephone-inbound::before { content: "\f5b8"; }
+.bi-telephone-minus-fill::before { content: "\f5b9"; }
+.bi-telephone-minus::before { content: "\f5ba"; }
+.bi-telephone-outbound-fill::before { content: "\f5bb"; }
+.bi-telephone-outbound::before { content: "\f5bc"; }
+.bi-telephone-plus-fill::before { content: "\f5bd"; }
+.bi-telephone-plus::before { content: "\f5be"; }
+.bi-telephone-x-fill::before { content: "\f5bf"; }
+.bi-telephone-x::before { content: "\f5c0"; }
+.bi-telephone::before { content: "\f5c1"; }
+.bi-terminal-fill::before { content: "\f5c2"; }
+.bi-terminal::before { content: "\f5c3"; }
+.bi-text-center::before { content: "\f5c4"; }
+.bi-text-indent-left::before { content: "\f5c5"; }
+.bi-text-indent-right::before { content: "\f5c6"; }
+.bi-text-left::before { content: "\f5c7"; }
+.bi-text-paragraph::before { content: "\f5c8"; }
+.bi-text-right::before { content: "\f5c9"; }
+.bi-textarea-resize::before { content: "\f5ca"; }
+.bi-textarea-t::before { content: "\f5cb"; }
+.bi-textarea::before { content: "\f5cc"; }
+.bi-thermometer-half::before { content: "\f5cd"; }
+.bi-thermometer-high::before { content: "\f5ce"; }
+.bi-thermometer-low::before { content: "\f5cf"; }
+.bi-thermometer-snow::before { content: "\f5d0"; }
+.bi-thermometer-sun::before { content: "\f5d1"; }
+.bi-thermometer::before { content: "\f5d2"; }
+.bi-three-dots-vertical::before { content: "\f5d3"; }
+.bi-three-dots::before { content: "\f5d4"; }
+.bi-toggle-off::before { content: "\f5d5"; }
+.bi-toggle-on::before { content: "\f5d6"; }
+.bi-toggle2-off::before { content: "\f5d7"; }
+.bi-toggle2-on::before { content: "\f5d8"; }
+.bi-toggles::before { content: "\f5d9"; }
+.bi-toggles2::before { content: "\f5da"; }
+.bi-tools::before { content: "\f5db"; }
+.bi-tornado::before { content: "\f5dc"; }
+.bi-trash-fill::before { content: "\f5dd"; }
+.bi-trash::before { content: "\f5de"; }
+.bi-trash2-fill::before { content: "\f5df"; }
+.bi-trash2::before { content: "\f5e0"; }
+.bi-tree-fill::before { content: "\f5e1"; }
+.bi-tree::before { content: "\f5e2"; }
+.bi-triangle-fill::before { content: "\f5e3"; }
+.bi-triangle-half::before { content: "\f5e4"; }
+.bi-triangle::before { content: "\f5e5"; }
+.bi-trophy-fill::before { content: "\f5e6"; }
+.bi-trophy::before { content: "\f5e7"; }
+.bi-tropical-storm::before { content: "\f5e8"; }
+.bi-truck-flatbed::before { content: "\f5e9"; }
+.bi-truck::before { content: "\f5ea"; }
+.bi-tsunami::before { content: "\f5eb"; }
+.bi-tv-fill::before { content: "\f5ec"; }
+.bi-tv::before { content: "\f5ed"; }
+.bi-twitch::before { content: "\f5ee"; }
+.bi-twitter::before { content: "\f5ef"; }
+.bi-type-bold::before { content: "\f5f0"; }
+.bi-type-h1::before { content: "\f5f1"; }
+.bi-type-h2::before { content: "\f5f2"; }
+.bi-type-h3::before { content: "\f5f3"; }
+.bi-type-italic::before { content: "\f5f4"; }
+.bi-type-strikethrough::before { content: "\f5f5"; }
+.bi-type-underline::before { content: "\f5f6"; }
+.bi-type::before { content: "\f5f7"; }
+.bi-ui-checks-grid::before { content: "\f5f8"; }
+.bi-ui-checks::before { content: "\f5f9"; }
+.bi-ui-radios-grid::before { content: "\f5fa"; }
+.bi-ui-radios::before { content: "\f5fb"; }
+.bi-umbrella-fill::before { content: "\f5fc"; }
+.bi-umbrella::before { content: "\f5fd"; }
+.bi-union::before { content: "\f5fe"; }
+.bi-unlock-fill::before { content: "\f5ff"; }
+.bi-unlock::before { content: "\f600"; }
+.bi-upc-scan::before { content: "\f601"; }
+.bi-upc::before { content: "\f602"; }
+.bi-upload::before { content: "\f603"; }
+.bi-vector-pen::before { content: "\f604"; }
+.bi-view-list::before { content: "\f605"; }
+.bi-view-stacked::before { content: "\f606"; }
+.bi-vinyl-fill::before { content: "\f607"; }
+.bi-vinyl::before { content: "\f608"; }
+.bi-voicemail::before { content: "\f609"; }
+.bi-volume-down-fill::before { content: "\f60a"; }
+.bi-volume-down::before { content: "\f60b"; }
+.bi-volume-mute-fill::before { content: "\f60c"; }
+.bi-volume-mute::before { content: "\f60d"; }
+.bi-volume-off-fill::before { content: "\f60e"; }
+.bi-volume-off::before { content: "\f60f"; }
+.bi-volume-up-fill::before { content: "\f610"; }
+.bi-volume-up::before { content: "\f611"; }
+.bi-vr::before { content: "\f612"; }
+.bi-wallet-fill::before { content: "\f613"; }
+.bi-wallet::before { content: "\f614"; }
+.bi-wallet2::before { content: "\f615"; }
+.bi-watch::before { content: "\f616"; }
+.bi-water::before { content: "\f617"; }
+.bi-whatsapp::before { content: "\f618"; }
+.bi-wifi-1::before { content: "\f619"; }
+.bi-wifi-2::before { content: "\f61a"; }
+.bi-wifi-off::before { content: "\f61b"; }
+.bi-wifi::before { content: "\f61c"; }
+.bi-wind::before { content: "\f61d"; }
+.bi-window-dock::before { content: "\f61e"; }
+.bi-window-sidebar::before { content: "\f61f"; }
+.bi-window::before { content: "\f620"; }
+.bi-wrench::before { content: "\f621"; }
+.bi-x-circle-fill::before { content: "\f622"; }
+.bi-x-circle::before { content: "\f623"; }
+.bi-x-diamond-fill::before { content: "\f624"; }
+.bi-x-diamond::before { content: "\f625"; }
+.bi-x-octagon-fill::before { content: "\f626"; }
+.bi-x-octagon::before { content: "\f627"; }
+.bi-x-square-fill::before { content: "\f628"; }
+.bi-x-square::before { content: "\f629"; }
+.bi-x::before { content: "\f62a"; }
+.bi-youtube::before { content: "\f62b"; }
+.bi-zoom-in::before { content: "\f62c"; }
+.bi-zoom-out::before { content: "\f62d"; }
+.bi-bank::before { content: "\f62e"; }
+.bi-bank2::before { content: "\f62f"; }
+.bi-bell-slash-fill::before { content: "\f630"; }
+.bi-bell-slash::before { content: "\f631"; }
+.bi-cash-coin::before { content: "\f632"; }
+.bi-check-lg::before { content: "\f633"; }
+.bi-coin::before { content: "\f634"; }
+.bi-currency-bitcoin::before { content: "\f635"; }
+.bi-currency-dollar::before { content: "\f636"; }
+.bi-currency-euro::before { content: "\f637"; }
+.bi-currency-exchange::before { content: "\f638"; }
+.bi-currency-pound::before { content: "\f639"; }
+.bi-currency-yen::before { content: "\f63a"; }
+.bi-dash-lg::before { content: "\f63b"; }
+.bi-exclamation-lg::before { content: "\f63c"; }
+.bi-file-earmark-pdf-fill::before { content: "\f63d"; }
+.bi-file-earmark-pdf::before { content: "\f63e"; }
+.bi-file-pdf-fill::before { content: "\f63f"; }
+.bi-file-pdf::before { content: "\f640"; }
+.bi-gender-ambiguous::before { content: "\f641"; }
+.bi-gender-female::before { content: "\f642"; }
+.bi-gender-male::before { content: "\f643"; }
+.bi-gender-trans::before { content: "\f644"; }
+.bi-headset-vr::before { content: "\f645"; }
+.bi-info-lg::before { content: "\f646"; }
+.bi-mastodon::before { content: "\f647"; }
+.bi-messenger::before { content: "\f648"; }
+.bi-piggy-bank-fill::before { content: "\f649"; }
+.bi-piggy-bank::before { content: "\f64a"; }
+.bi-pin-map-fill::before { content: "\f64b"; }
+.bi-pin-map::before { content: "\f64c"; }
+.bi-plus-lg::before { content: "\f64d"; }
+.bi-question-lg::before { content: "\f64e"; }
+.bi-recycle::before { content: "\f64f"; }
+.bi-reddit::before { content: "\f650"; }
+.bi-safe-fill::before { content: "\f651"; }
+.bi-safe2-fill::before { content: "\f652"; }
+.bi-safe2::before { content: "\f653"; }
+.bi-sd-card-fill::before { content: "\f654"; }
+.bi-sd-card::before { content: "\f655"; }
+.bi-skype::before { content: "\f656"; }
+.bi-slash-lg::before { content: "\f657"; }
+.bi-translate::before { content: "\f658"; }
+.bi-x-lg::before { content: "\f659"; }
+.bi-safe::before { content: "\f65a"; }
+.bi-apple::before { content: "\f65b"; }
+.bi-microsoft::before { content: "\f65d"; }
+.bi-windows::before { content: "\f65e"; }
+.bi-behance::before { content: "\f65c"; }
+.bi-dribbble::before { content: "\f65f"; }
+.bi-line::before { content: "\f660"; }
+.bi-medium::before { content: "\f661"; }
+.bi-paypal::before { content: "\f662"; }
+.bi-pinterest::before { content: "\f663"; }
+.bi-signal::before { content: "\f664"; }
+.bi-snapchat::before { content: "\f665"; }
+.bi-spotify::before { content: "\f666"; }
+.bi-stack-overflow::before { content: "\f667"; }
+.bi-strava::before { content: "\f668"; }
+.bi-wordpress::before { content: "\f669"; }
+.bi-vimeo::before { content: "\f66a"; }
+.bi-activity::before { content: "\f66b"; }
+.bi-easel2-fill::before { content: "\f66c"; }
+.bi-easel2::before { content: "\f66d"; }
+.bi-easel3-fill::before { content: "\f66e"; }
+.bi-easel3::before { content: "\f66f"; }
+.bi-fan::before { content: "\f670"; }
+.bi-fingerprint::before { content: "\f671"; }
+.bi-graph-down-arrow::before { content: "\f672"; }
+.bi-graph-up-arrow::before { content: "\f673"; }
+.bi-hypnotize::before { content: "\f674"; }
+.bi-magic::before { content: "\f675"; }
+.bi-person-rolodex::before { content: "\f676"; }
+.bi-person-video::before { content: "\f677"; }
+.bi-person-video2::before { content: "\f678"; }
+.bi-person-video3::before { content: "\f679"; }
+.bi-person-workspace::before { content: "\f67a"; }
+.bi-radioactive::before { content: "\f67b"; }
+.bi-webcam-fill::before { content: "\f67c"; }
+.bi-webcam::before { content: "\f67d"; }
+.bi-yin-yang::before { content: "\f67e"; }
+.bi-bandaid-fill::before { content: "\f680"; }
+.bi-bandaid::before { content: "\f681"; }
+.bi-bluetooth::before { content: "\f682"; }
+.bi-body-text::before { content: "\f683"; }
+.bi-boombox::before { content: "\f684"; }
+.bi-boxes::before { content: "\f685"; }
+.bi-dpad-fill::before { content: "\f686"; }
+.bi-dpad::before { content: "\f687"; }
+.bi-ear-fill::before { content: "\f688"; }
+.bi-ear::before { content: "\f689"; }
+.bi-envelope-check-fill::before { content: "\f68b"; }
+.bi-envelope-check::before { content: "\f68c"; }
+.bi-envelope-dash-fill::before { content: "\f68e"; }
+.bi-envelope-dash::before { content: "\f68f"; }
+.bi-envelope-exclamation-fill::before { content: "\f691"; }
+.bi-envelope-exclamation::before { content: "\f692"; }
+.bi-envelope-plus-fill::before { content: "\f693"; }
+.bi-envelope-plus::before { content: "\f694"; }
+.bi-envelope-slash-fill::before { content: "\f696"; }
+.bi-envelope-slash::before { content: "\f697"; }
+.bi-envelope-x-fill::before { content: "\f699"; }
+.bi-envelope-x::before { content: "\f69a"; }
+.bi-explicit-fill::before { content: "\f69b"; }
+.bi-explicit::before { content: "\f69c"; }
+.bi-git::before { content: "\f69d"; }
+.bi-infinity::before { content: "\f69e"; }
+.bi-list-columns-reverse::before { content: "\f69f"; }
+.bi-list-columns::before { content: "\f6a0"; }
+.bi-meta::before { content: "\f6a1"; }
+.bi-nintendo-switch::before { content: "\f6a4"; }
+.bi-pc-display-horizontal::before { content: "\f6a5"; }
+.bi-pc-display::before { content: "\f6a6"; }
+.bi-pc-horizontal::before { content: "\f6a7"; }
+.bi-pc::before { content: "\f6a8"; }
+.bi-playstation::before { content: "\f6a9"; }
+.bi-plus-slash-minus::before { content: "\f6aa"; }
+.bi-projector-fill::before { content: "\f6ab"; }
+.bi-projector::before { content: "\f6ac"; }
+.bi-qr-code-scan::before { content: "\f6ad"; }
+.bi-qr-code::before { content: "\f6ae"; }
+.bi-quora::before { content: "\f6af"; }
+.bi-quote::before { content: "\f6b0"; }
+.bi-robot::before { content: "\f6b1"; }
+.bi-send-check-fill::before { content: "\f6b2"; }
+.bi-send-check::before { content: "\f6b3"; }
+.bi-send-dash-fill::before { content: "\f6b4"; }
+.bi-send-dash::before { content: "\f6b5"; }
+.bi-send-exclamation-fill::before { content: "\f6b7"; }
+.bi-send-exclamation::before { content: "\f6b8"; }
+.bi-send-fill::before { content: "\f6b9"; }
+.bi-send-plus-fill::before { content: "\f6ba"; }
+.bi-send-plus::before { content: "\f6bb"; }
+.bi-send-slash-fill::before { content: "\f6bc"; }
+.bi-send-slash::before { content: "\f6bd"; }
+.bi-send-x-fill::before { content: "\f6be"; }
+.bi-send-x::before { content: "\f6bf"; }
+.bi-send::before { content: "\f6c0"; }
+.bi-steam::before { content: "\f6c1"; }
+.bi-terminal-dash::before { content: "\f6c3"; }
+.bi-terminal-plus::before { content: "\f6c4"; }
+.bi-terminal-split::before { content: "\f6c5"; }
+.bi-ticket-detailed-fill::before { content: "\f6c6"; }
+.bi-ticket-detailed::before { content: "\f6c7"; }
+.bi-ticket-fill::before { content: "\f6c8"; }
+.bi-ticket-perforated-fill::before { content: "\f6c9"; }
+.bi-ticket-perforated::before { content: "\f6ca"; }
+.bi-ticket::before { content: "\f6cb"; }
+.bi-tiktok::before { content: "\f6cc"; }
+.bi-window-dash::before { content: "\f6cd"; }
+.bi-window-desktop::before { content: "\f6ce"; }
+.bi-window-fullscreen::before { content: "\f6cf"; }
+.bi-window-plus::before { content: "\f6d0"; }
+.bi-window-split::before { content: "\f6d1"; }
+.bi-window-stack::before { content: "\f6d2"; }
+.bi-window-x::before { content: "\f6d3"; }
+.bi-xbox::before { content: "\f6d4"; }
+.bi-ethernet::before { content: "\f6d5"; }
+.bi-hdmi-fill::before { content: "\f6d6"; }
+.bi-hdmi::before { content: "\f6d7"; }
+.bi-usb-c-fill::before { content: "\f6d8"; }
+.bi-usb-c::before { content: "\f6d9"; }
+.bi-usb-fill::before { content: "\f6da"; }
+.bi-usb-plug-fill::before { content: "\f6db"; }
+.bi-usb-plug::before { content: "\f6dc"; }
+.bi-usb-symbol::before { content: "\f6dd"; }
+.bi-usb::before { content: "\f6de"; }
+.bi-boombox-fill::before { content: "\f6df"; }
+.bi-displayport::before { content: "\f6e1"; }
+.bi-gpu-card::before { content: "\f6e2"; }
+.bi-memory::before { content: "\f6e3"; }
+.bi-modem-fill::before { content: "\f6e4"; }
+.bi-modem::before { content: "\f6e5"; }
+.bi-motherboard-fill::before { content: "\f6e6"; }
+.bi-motherboard::before { content: "\f6e7"; }
+.bi-optical-audio-fill::before { content: "\f6e8"; }
+.bi-optical-audio::before { content: "\f6e9"; }
+.bi-pci-card::before { content: "\f6ea"; }
+.bi-router-fill::before { content: "\f6eb"; }
+.bi-router::before { content: "\f6ec"; }
+.bi-thunderbolt-fill::before { content: "\f6ef"; }
+.bi-thunderbolt::before { content: "\f6f0"; }
+.bi-usb-drive-fill::before { content: "\f6f1"; }
+.bi-usb-drive::before { content: "\f6f2"; }
+.bi-usb-micro-fill::before { content: "\f6f3"; }
+.bi-usb-micro::before { content: "\f6f4"; }
+.bi-usb-mini-fill::before { content: "\f6f5"; }
+.bi-usb-mini::before { content: "\f6f6"; }
+.bi-cloud-haze2::before { content: "\f6f7"; }
+.bi-device-hdd-fill::before { content: "\f6f8"; }
+.bi-device-hdd::before { content: "\f6f9"; }
+.bi-device-ssd-fill::before { content: "\f6fa"; }
+.bi-device-ssd::before { content: "\f6fb"; }
+.bi-displayport-fill::before { content: "\f6fc"; }
+.bi-mortarboard-fill::before { content: "\f6fd"; }
+.bi-mortarboard::before { content: "\f6fe"; }
+.bi-terminal-x::before { content: "\f6ff"; }
+.bi-arrow-through-heart-fill::before { content: "\f700"; }
+.bi-arrow-through-heart::before { content: "\f701"; }
+.bi-badge-sd-fill::before { content: "\f702"; }
+.bi-badge-sd::before { content: "\f703"; }
+.bi-bag-heart-fill::before { content: "\f704"; }
+.bi-bag-heart::before { content: "\f705"; }
+.bi-balloon-fill::before { content: "\f706"; }
+.bi-balloon-heart-fill::before { content: "\f707"; }
+.bi-balloon-heart::before { content: "\f708"; }
+.bi-balloon::before { content: "\f709"; }
+.bi-box2-fill::before { content: "\f70a"; }
+.bi-box2-heart-fill::before { content: "\f70b"; }
+.bi-box2-heart::before { content: "\f70c"; }
+.bi-box2::before { content: "\f70d"; }
+.bi-braces-asterisk::before { content: "\f70e"; }
+.bi-calendar-heart-fill::before { content: "\f70f"; }
+.bi-calendar-heart::before { content: "\f710"; }
+.bi-calendar2-heart-fill::before { content: "\f711"; }
+.bi-calendar2-heart::before { content: "\f712"; }
+.bi-chat-heart-fill::before { content: "\f713"; }
+.bi-chat-heart::before { content: "\f714"; }
+.bi-chat-left-heart-fill::before { content: "\f715"; }
+.bi-chat-left-heart::before { content: "\f716"; }
+.bi-chat-right-heart-fill::before { content: "\f717"; }
+.bi-chat-right-heart::before { content: "\f718"; }
+.bi-chat-square-heart-fill::before { content: "\f719"; }
+.bi-chat-square-heart::before { content: "\f71a"; }
+.bi-clipboard-check-fill::before { content: "\f71b"; }
+.bi-clipboard-data-fill::before { content: "\f71c"; }
+.bi-clipboard-fill::before { content: "\f71d"; }
+.bi-clipboard-heart-fill::before { content: "\f71e"; }
+.bi-clipboard-heart::before { content: "\f71f"; }
+.bi-clipboard-minus-fill::before { content: "\f720"; }
+.bi-clipboard-plus-fill::before { content: "\f721"; }
+.bi-clipboard-pulse::before { content: "\f722"; }
+.bi-clipboard-x-fill::before { content: "\f723"; }
+.bi-clipboard2-check-fill::before { content: "\f724"; }
+.bi-clipboard2-check::before { content: "\f725"; }
+.bi-clipboard2-data-fill::before { content: "\f726"; }
+.bi-clipboard2-data::before { content: "\f727"; }
+.bi-clipboard2-fill::before { content: "\f728"; }
+.bi-clipboard2-heart-fill::before { content: "\f729"; }
+.bi-clipboard2-heart::before { content: "\f72a"; }
+.bi-clipboard2-minus-fill::before { content: "\f72b"; }
+.bi-clipboard2-minus::before { content: "\f72c"; }
+.bi-clipboard2-plus-fill::before { content: "\f72d"; }
+.bi-clipboard2-plus::before { content: "\f72e"; }
+.bi-clipboard2-pulse-fill::before { content: "\f72f"; }
+.bi-clipboard2-pulse::before { content: "\f730"; }
+.bi-clipboard2-x-fill::before { content: "\f731"; }
+.bi-clipboard2-x::before { content: "\f732"; }
+.bi-clipboard2::before { content: "\f733"; }
+.bi-emoji-kiss-fill::before { content: "\f734"; }
+.bi-emoji-kiss::before { content: "\f735"; }
+.bi-envelope-heart-fill::before { content: "\f736"; }
+.bi-envelope-heart::before { content: "\f737"; }
+.bi-envelope-open-heart-fill::before { content: "\f738"; }
+.bi-envelope-open-heart::before { content: "\f739"; }
+.bi-envelope-paper-fill::before { content: "\f73a"; }
+.bi-envelope-paper-heart-fill::before { content: "\f73b"; }
+.bi-envelope-paper-heart::before { content: "\f73c"; }
+.bi-envelope-paper::before { content: "\f73d"; }
+.bi-filetype-aac::before { content: "\f73e"; }
+.bi-filetype-ai::before { content: "\f73f"; }
+.bi-filetype-bmp::before { content: "\f740"; }
+.bi-filetype-cs::before { content: "\f741"; }
+.bi-filetype-css::before { content: "\f742"; }
+.bi-filetype-csv::before { content: "\f743"; }
+.bi-filetype-doc::before { content: "\f744"; }
+.bi-filetype-docx::before { content: "\f745"; }
+.bi-filetype-exe::before { content: "\f746"; }
+.bi-filetype-gif::before { content: "\f747"; }
+.bi-filetype-heic::before { content: "\f748"; }
+.bi-filetype-html::before { content: "\f749"; }
+.bi-filetype-java::before { content: "\f74a"; }
+.bi-filetype-jpg::before { content: "\f74b"; }
+.bi-filetype-js::before { content: "\f74c"; }
+.bi-filetype-jsx::before { content: "\f74d"; }
+.bi-filetype-key::before { content: "\f74e"; }
+.bi-filetype-m4p::before { content: "\f74f"; }
+.bi-filetype-md::before { content: "\f750"; }
+.bi-filetype-mdx::before { content: "\f751"; }
+.bi-filetype-mov::before { content: "\f752"; }
+.bi-filetype-mp3::before { content: "\f753"; }
+.bi-filetype-mp4::before { content: "\f754"; }
+.bi-filetype-otf::before { content: "\f755"; }
+.bi-filetype-pdf::before { content: "\f756"; }
+.bi-filetype-php::before { content: "\f757"; }
+.bi-filetype-png::before { content: "\f758"; }
+.bi-filetype-ppt::before { content: "\f75a"; }
+.bi-filetype-psd::before { content: "\f75b"; }
+.bi-filetype-py::before { content: "\f75c"; }
+.bi-filetype-raw::before { content: "\f75d"; }
+.bi-filetype-rb::before { content: "\f75e"; }
+.bi-filetype-sass::before { content: "\f75f"; }
+.bi-filetype-scss::before { content: "\f760"; }
+.bi-filetype-sh::before { content: "\f761"; }
+.bi-filetype-svg::before { content: "\f762"; }
+.bi-filetype-tiff::before { content: "\f763"; }
+.bi-filetype-tsx::before { content: "\f764"; }
+.bi-filetype-ttf::before { content: "\f765"; }
+.bi-filetype-txt::before { content: "\f766"; }
+.bi-filetype-wav::before { content: "\f767"; }
+.bi-filetype-woff::before { content: "\f768"; }
+.bi-filetype-xls::before { content: "\f76a"; }
+.bi-filetype-xml::before { content: "\f76b"; }
+.bi-filetype-yml::before { content: "\f76c"; }
+.bi-heart-arrow::before { content: "\f76d"; }
+.bi-heart-pulse-fill::before { content: "\f76e"; }
+.bi-heart-pulse::before { content: "\f76f"; }
+.bi-heartbreak-fill::before { content: "\f770"; }
+.bi-heartbreak::before { content: "\f771"; }
+.bi-hearts::before { content: "\f772"; }
+.bi-hospital-fill::before { content: "\f773"; }
+.bi-hospital::before { content: "\f774"; }
+.bi-house-heart-fill::before { content: "\f775"; }
+.bi-house-heart::before { content: "\f776"; }
+.bi-incognito::before { content: "\f777"; }
+.bi-magnet-fill::before { content: "\f778"; }
+.bi-magnet::before { content: "\f779"; }
+.bi-person-heart::before { content: "\f77a"; }
+.bi-person-hearts::before { content: "\f77b"; }
+.bi-phone-flip::before { content: "\f77c"; }
+.bi-plugin::before { content: "\f77d"; }
+.bi-postage-fill::before { content: "\f77e"; }
+.bi-postage-heart-fill::before { content: "\f77f"; }
+.bi-postage-heart::before { content: "\f780"; }
+.bi-postage::before { content: "\f781"; }
+.bi-postcard-fill::before { content: "\f782"; }
+.bi-postcard-heart-fill::before { content: "\f783"; }
+.bi-postcard-heart::before { content: "\f784"; }
+.bi-postcard::before { content: "\f785"; }
+.bi-search-heart-fill::before { content: "\f786"; }
+.bi-search-heart::before { content: "\f787"; }
+.bi-sliders2-vertical::before { content: "\f788"; }
+.bi-sliders2::before { content: "\f789"; }
+.bi-trash3-fill::before { content: "\f78a"; }
+.bi-trash3::before { content: "\f78b"; }
+.bi-valentine::before { content: "\f78c"; }
+.bi-valentine2::before { content: "\f78d"; }
+.bi-wrench-adjustable-circle-fill::before { content: "\f78e"; }
+.bi-wrench-adjustable-circle::before { content: "\f78f"; }
+.bi-wrench-adjustable::before { content: "\f790"; }
+.bi-filetype-json::before { content: "\f791"; }
+.bi-filetype-pptx::before { content: "\f792"; }
+.bi-filetype-xlsx::before { content: "\f793"; }
+.bi-1-circle-fill::before { content: "\f796"; }
+.bi-1-circle::before { content: "\f797"; }
+.bi-1-square-fill::before { content: "\f798"; }
+.bi-1-square::before { content: "\f799"; }
+.bi-2-circle-fill::before { content: "\f79c"; }
+.bi-2-circle::before { content: "\f79d"; }
+.bi-2-square-fill::before { content: "\f79e"; }
+.bi-2-square::before { content: "\f79f"; }
+.bi-3-circle-fill::before { content: "\f7a2"; }
+.bi-3-circle::before { content: "\f7a3"; }
+.bi-3-square-fill::before { content: "\f7a4"; }
+.bi-3-square::before { content: "\f7a5"; }
+.bi-4-circle-fill::before { content: "\f7a8"; }
+.bi-4-circle::before { content: "\f7a9"; }
+.bi-4-square-fill::before { content: "\f7aa"; }
+.bi-4-square::before { content: "\f7ab"; }
+.bi-5-circle-fill::before { content: "\f7ae"; }
+.bi-5-circle::before { content: "\f7af"; }
+.bi-5-square-fill::before { content: "\f7b0"; }
+.bi-5-square::before { content: "\f7b1"; }
+.bi-6-circle-fill::before { content: "\f7b4"; }
+.bi-6-circle::before { content: "\f7b5"; }
+.bi-6-square-fill::before { content: "\f7b6"; }
+.bi-6-square::before { content: "\f7b7"; }
+.bi-7-circle-fill::before { content: "\f7ba"; }
+.bi-7-circle::before { content: "\f7bb"; }
+.bi-7-square-fill::before { content: "\f7bc"; }
+.bi-7-square::before { content: "\f7bd"; }
+.bi-8-circle-fill::before { content: "\f7c0"; }
+.bi-8-circle::before { content: "\f7c1"; }
+.bi-8-square-fill::before { content: "\f7c2"; }
+.bi-8-square::before { content: "\f7c3"; }
+.bi-9-circle-fill::before { content: "\f7c6"; }
+.bi-9-circle::before { content: "\f7c7"; }
+.bi-9-square-fill::before { content: "\f7c8"; }
+.bi-9-square::before { content: "\f7c9"; }
+.bi-airplane-engines-fill::before { content: "\f7ca"; }
+.bi-airplane-engines::before { content: "\f7cb"; }
+.bi-airplane-fill::before { content: "\f7cc"; }
+.bi-airplane::before { content: "\f7cd"; }
+.bi-alexa::before { content: "\f7ce"; }
+.bi-alipay::before { content: "\f7cf"; }
+.bi-android::before { content: "\f7d0"; }
+.bi-android2::before { content: "\f7d1"; }
+.bi-box-fill::before { content: "\f7d2"; }
+.bi-box-seam-fill::before { content: "\f7d3"; }
+.bi-browser-chrome::before { content: "\f7d4"; }
+.bi-browser-edge::before { content: "\f7d5"; }
+.bi-browser-firefox::before { content: "\f7d6"; }
+.bi-browser-safari::before { content: "\f7d7"; }
+.bi-c-circle-fill::before { content: "\f7da"; }
+.bi-c-circle::before { content: "\f7db"; }
+.bi-c-square-fill::before { content: "\f7dc"; }
+.bi-c-square::before { content: "\f7dd"; }
+.bi-capsule-pill::before { content: "\f7de"; }
+.bi-capsule::before { content: "\f7df"; }
+.bi-car-front-fill::before { content: "\f7e0"; }
+.bi-car-front::before { content: "\f7e1"; }
+.bi-cassette-fill::before { content: "\f7e2"; }
+.bi-cassette::before { content: "\f7e3"; }
+.bi-cc-circle-fill::before { content: "\f7e6"; }
+.bi-cc-circle::before { content: "\f7e7"; }
+.bi-cc-square-fill::before { content: "\f7e8"; }
+.bi-cc-square::before { content: "\f7e9"; }
+.bi-cup-hot-fill::before { content: "\f7ea"; }
+.bi-cup-hot::before { content: "\f7eb"; }
+.bi-currency-rupee::before { content: "\f7ec"; }
+.bi-dropbox::before { content: "\f7ed"; }
+.bi-escape::before { content: "\f7ee"; }
+.bi-fast-forward-btn-fill::before { content: "\f7ef"; }
+.bi-fast-forward-btn::before { content: "\f7f0"; }
+.bi-fast-forward-circle-fill::before { content: "\f7f1"; }
+.bi-fast-forward-circle::before { content: "\f7f2"; }
+.bi-fast-forward-fill::before { content: "\f7f3"; }
+.bi-fast-forward::before { content: "\f7f4"; }
+.bi-filetype-sql::before { content: "\f7f5"; }
+.bi-fire::before { content: "\f7f6"; }
+.bi-google-play::before { content: "\f7f7"; }
+.bi-h-circle-fill::before { content: "\f7fa"; }
+.bi-h-circle::before { content: "\f7fb"; }
+.bi-h-square-fill::before { content: "\f7fc"; }
+.bi-h-square::before { content: "\f7fd"; }
+.bi-indent::before { content: "\f7fe"; }
+.bi-lungs-fill::before { content: "\f7ff"; }
+.bi-lungs::before { content: "\f800"; }
+.bi-microsoft-teams::before { content: "\f801"; }
+.bi-p-circle-fill::before { content: "\f804"; }
+.bi-p-circle::before { content: "\f805"; }
+.bi-p-square-fill::before { content: "\f806"; }
+.bi-p-square::before { content: "\f807"; }
+.bi-pass-fill::before { content: "\f808"; }
+.bi-pass::before { content: "\f809"; }
+.bi-prescription::before { content: "\f80a"; }
+.bi-prescription2::before { content: "\f80b"; }
+.bi-r-circle-fill::before { content: "\f80e"; }
+.bi-r-circle::before { content: "\f80f"; }
+.bi-r-square-fill::before { content: "\f810"; }
+.bi-r-square::before { content: "\f811"; }
+.bi-repeat-1::before { content: "\f812"; }
+.bi-repeat::before { content: "\f813"; }
+.bi-rewind-btn-fill::before { content: "\f814"; }
+.bi-rewind-btn::before { content: "\f815"; }
+.bi-rewind-circle-fill::before { content: "\f816"; }
+.bi-rewind-circle::before { content: "\f817"; }
+.bi-rewind-fill::before { content: "\f818"; }
+.bi-rewind::before { content: "\f819"; }
+.bi-train-freight-front-fill::before { content: "\f81a"; }
+.bi-train-freight-front::before { content: "\f81b"; }
+.bi-train-front-fill::before { content: "\f81c"; }
+.bi-train-front::before { content: "\f81d"; }
+.bi-train-lightrail-front-fill::before { content: "\f81e"; }
+.bi-train-lightrail-front::before { content: "\f81f"; }
+.bi-truck-front-fill::before { content: "\f820"; }
+.bi-truck-front::before { content: "\f821"; }
+.bi-ubuntu::before { content: "\f822"; }
+.bi-unindent::before { content: "\f823"; }
+.bi-unity::before { content: "\f824"; }
+.bi-universal-access-circle::before { content: "\f825"; }
+.bi-universal-access::before { content: "\f826"; }
+.bi-virus::before { content: "\f827"; }
+.bi-virus2::before { content: "\f828"; }
+.bi-wechat::before { content: "\f829"; }
+.bi-yelp::before { content: "\f82a"; }
+.bi-sign-stop-fill::before { content: "\f82b"; }
+.bi-sign-stop-lights-fill::before { content: "\f82c"; }
+.bi-sign-stop-lights::before { content: "\f82d"; }
+.bi-sign-stop::before { content: "\f82e"; }
+.bi-sign-turn-left-fill::before { content: "\f82f"; }
+.bi-sign-turn-left::before { content: "\f830"; }
+.bi-sign-turn-right-fill::before { content: "\f831"; }
+.bi-sign-turn-right::before { content: "\f832"; }
+.bi-sign-turn-slight-left-fill::before { content: "\f833"; }
+.bi-sign-turn-slight-left::before { content: "\f834"; }
+.bi-sign-turn-slight-right-fill::before { content: "\f835"; }
+.bi-sign-turn-slight-right::before { content: "\f836"; }
+.bi-sign-yield-fill::before { content: "\f837"; }
+.bi-sign-yield::before { content: "\f838"; }
+.bi-ev-station-fill::before { content: "\f839"; }
+.bi-ev-station::before { content: "\f83a"; }
+.bi-fuel-pump-diesel-fill::before { content: "\f83b"; }
+.bi-fuel-pump-diesel::before { content: "\f83c"; }
+.bi-fuel-pump-fill::before { content: "\f83d"; }
+.bi-fuel-pump::before { content: "\f83e"; }
+.bi-0-circle-fill::before { content: "\f83f"; }
+.bi-0-circle::before { content: "\f840"; }
+.bi-0-square-fill::before { content: "\f841"; }
+.bi-0-square::before { content: "\f842"; }
+.bi-rocket-fill::before { content: "\f843"; }
+.bi-rocket-takeoff-fill::before { content: "\f844"; }
+.bi-rocket-takeoff::before { content: "\f845"; }
+.bi-rocket::before { content: "\f846"; }
+.bi-stripe::before { content: "\f847"; }
+.bi-subscript::before { content: "\f848"; }
+.bi-superscript::before { content: "\f849"; }
+.bi-trello::before { content: "\f84a"; }
+.bi-envelope-at-fill::before { content: "\f84b"; }
+.bi-envelope-at::before { content: "\f84c"; }
+.bi-regex::before { content: "\f84d"; }
+.bi-text-wrap::before { content: "\f84e"; }
+.bi-sign-dead-end-fill::before { content: "\f84f"; }
+.bi-sign-dead-end::before { content: "\f850"; }
+.bi-sign-do-not-enter-fill::before { content: "\f851"; }
+.bi-sign-do-not-enter::before { content: "\f852"; }
+.bi-sign-intersection-fill::before { content: "\f853"; }
+.bi-sign-intersection-side-fill::before { content: "\f854"; }
+.bi-sign-intersection-side::before { content: "\f855"; }
+.bi-sign-intersection-t-fill::before { content: "\f856"; }
+.bi-sign-intersection-t::before { content: "\f857"; }
+.bi-sign-intersection-y-fill::before { content: "\f858"; }
+.bi-sign-intersection-y::before { content: "\f859"; }
+.bi-sign-intersection::before { content: "\f85a"; }
+.bi-sign-merge-left-fill::before { content: "\f85b"; }
+.bi-sign-merge-left::before { content: "\f85c"; }
+.bi-sign-merge-right-fill::before { content: "\f85d"; }
+.bi-sign-merge-right::before { content: "\f85e"; }
+.bi-sign-no-left-turn-fill::before { content: "\f85f"; }
+.bi-sign-no-left-turn::before { content: "\f860"; }
+.bi-sign-no-parking-fill::before { content: "\f861"; }
+.bi-sign-no-parking::before { content: "\f862"; }
+.bi-sign-no-right-turn-fill::before { content: "\f863"; }
+.bi-sign-no-right-turn::before { content: "\f864"; }
+.bi-sign-railroad-fill::before { content: "\f865"; }
+.bi-sign-railroad::before { content: "\f866"; }
+.bi-building-add::before { content: "\f867"; }
+.bi-building-check::before { content: "\f868"; }
+.bi-building-dash::before { content: "\f869"; }
+.bi-building-down::before { content: "\f86a"; }
+.bi-building-exclamation::before { content: "\f86b"; }
+.bi-building-fill-add::before { content: "\f86c"; }
+.bi-building-fill-check::before { content: "\f86d"; }
+.bi-building-fill-dash::before { content: "\f86e"; }
+.bi-building-fill-down::before { content: "\f86f"; }
+.bi-building-fill-exclamation::before { content: "\f870"; }
+.bi-building-fill-gear::before { content: "\f871"; }
+.bi-building-fill-lock::before { content: "\f872"; }
+.bi-building-fill-slash::before { content: "\f873"; }
+.bi-building-fill-up::before { content: "\f874"; }
+.bi-building-fill-x::before { content: "\f875"; }
+.bi-building-fill::before { content: "\f876"; }
+.bi-building-gear::before { content: "\f877"; }
+.bi-building-lock::before { content: "\f878"; }
+.bi-building-slash::before { content: "\f879"; }
+.bi-building-up::before { content: "\f87a"; }
+.bi-building-x::before { content: "\f87b"; }
+.bi-buildings-fill::before { content: "\f87c"; }
+.bi-buildings::before { content: "\f87d"; }
+.bi-bus-front-fill::before { content: "\f87e"; }
+.bi-bus-front::before { content: "\f87f"; }
+.bi-ev-front-fill::before { content: "\f880"; }
+.bi-ev-front::before { content: "\f881"; }
+.bi-globe-americas::before { content: "\f882"; }
+.bi-globe-asia-australia::before { content: "\f883"; }
+.bi-globe-central-south-asia::before { content: "\f884"; }
+.bi-globe-europe-africa::before { content: "\f885"; }
+.bi-house-add-fill::before { content: "\f886"; }
+.bi-house-add::before { content: "\f887"; }
+.bi-house-check-fill::before { content: "\f888"; }
+.bi-house-check::before { content: "\f889"; }
+.bi-house-dash-fill::before { content: "\f88a"; }
+.bi-house-dash::before { content: "\f88b"; }
+.bi-house-down-fill::before { content: "\f88c"; }
+.bi-house-down::before { content: "\f88d"; }
+.bi-house-exclamation-fill::before { content: "\f88e"; }
+.bi-house-exclamation::before { content: "\f88f"; }
+.bi-house-gear-fill::before { content: "\f890"; }
+.bi-house-gear::before { content: "\f891"; }
+.bi-house-lock-fill::before { content: "\f892"; }
+.bi-house-lock::before { content: "\f893"; }
+.bi-house-slash-fill::before { content: "\f894"; }
+.bi-house-slash::before { content: "\f895"; }
+.bi-house-up-fill::before { content: "\f896"; }
+.bi-house-up::before { content: "\f897"; }
+.bi-house-x-fill::before { content: "\f898"; }
+.bi-house-x::before { content: "\f899"; }
+.bi-person-add::before { content: "\f89a"; }
+.bi-person-down::before { content: "\f89b"; }
+.bi-person-exclamation::before { content: "\f89c"; }
+.bi-person-fill-add::before { content: "\f89d"; }
+.bi-person-fill-check::before { content: "\f89e"; }
+.bi-person-fill-dash::before { content: "\f89f"; }
+.bi-person-fill-down::before { content: "\f8a0"; }
+.bi-person-fill-exclamation::before { content: "\f8a1"; }
+.bi-person-fill-gear::before { content: "\f8a2"; }
+.bi-person-fill-lock::before { content: "\f8a3"; }
+.bi-person-fill-slash::before { content: "\f8a4"; }
+.bi-person-fill-up::before { content: "\f8a5"; }
+.bi-person-fill-x::before { content: "\f8a6"; }
+.bi-person-gear::before { content: "\f8a7"; }
+.bi-person-lock::before { content: "\f8a8"; }
+.bi-person-slash::before { content: "\f8a9"; }
+.bi-person-up::before { content: "\f8aa"; }
+.bi-scooter::before { content: "\f8ab"; }
+.bi-taxi-front-fill::before { content: "\f8ac"; }
+.bi-taxi-front::before { content: "\f8ad"; }
+.bi-amd::before { content: "\f8ae"; }
+.bi-database-add::before { content: "\f8af"; }
+.bi-database-check::before { content: "\f8b0"; }
+.bi-database-dash::before { content: "\f8b1"; }
+.bi-database-down::before { content: "\f8b2"; }
+.bi-database-exclamation::before { content: "\f8b3"; }
+.bi-database-fill-add::before { content: "\f8b4"; }
+.bi-database-fill-check::before { content: "\f8b5"; }
+.bi-database-fill-dash::before { content: "\f8b6"; }
+.bi-database-fill-down::before { content: "\f8b7"; }
+.bi-database-fill-exclamation::before { content: "\f8b8"; }
+.bi-database-fill-gear::before { content: "\f8b9"; }
+.bi-database-fill-lock::before { content: "\f8ba"; }
+.bi-database-fill-slash::before { content: "\f8bb"; }
+.bi-database-fill-up::before { content: "\f8bc"; }
+.bi-database-fill-x::before { content: "\f8bd"; }
+.bi-database-fill::before { content: "\f8be"; }
+.bi-database-gear::before { content: "\f8bf"; }
+.bi-database-lock::before { content: "\f8c0"; }
+.bi-database-slash::before { content: "\f8c1"; }
+.bi-database-up::before { content: "\f8c2"; }
+.bi-database-x::before { content: "\f8c3"; }
+.bi-database::before { content: "\f8c4"; }
+.bi-houses-fill::before { content: "\f8c5"; }
+.bi-houses::before { content: "\f8c6"; }
+.bi-nvidia::before { content: "\f8c7"; }
+.bi-person-vcard-fill::before { content: "\f8c8"; }
+.bi-person-vcard::before { content: "\f8c9"; }
+.bi-sina-weibo::before { content: "\f8ca"; }
+.bi-tencent-qq::before { content: "\f8cb"; }
+.bi-wikipedia::before { content: "\f8cc"; }
+.bi-alphabet-uppercase::before { content: "\f2a5"; }
+.bi-alphabet::before { content: "\f68a"; }
+.bi-amazon::before { content: "\f68d"; }
+.bi-arrows-collapse-vertical::before { content: "\f690"; }
+.bi-arrows-expand-vertical::before { content: "\f695"; }
+.bi-arrows-vertical::before { content: "\f698"; }
+.bi-arrows::before { content: "\f6a2"; }
+.bi-ban-fill::before { content: "\f6a3"; }
+.bi-ban::before { content: "\f6b6"; }
+.bi-bing::before { content: "\f6c2"; }
+.bi-cake::before { content: "\f6e0"; }
+.bi-cake2::before { content: "\f6ed"; }
+.bi-cookie::before { content: "\f6ee"; }
+.bi-copy::before { content: "\f759"; }
+.bi-crosshair::before { content: "\f769"; }
+.bi-crosshair2::before { content: "\f794"; }
+.bi-emoji-astonished-fill::before { content: "\f795"; }
+.bi-emoji-astonished::before { content: "\f79a"; }
+.bi-emoji-grimace-fill::before { content: "\f79b"; }
+.bi-emoji-grimace::before { content: "\f7a0"; }
+.bi-emoji-grin-fill::before { content: "\f7a1"; }
+.bi-emoji-grin::before { content: "\f7a6"; }
+.bi-emoji-surprise-fill::before { content: "\f7a7"; }
+.bi-emoji-surprise::before { content: "\f7ac"; }
+.bi-emoji-tear-fill::before { content: "\f7ad"; }
+.bi-emoji-tear::before { content: "\f7b2"; }
+.bi-envelope-arrow-down-fill::before { content: "\f7b3"; }
+.bi-envelope-arrow-down::before { content: "\f7b8"; }
+.bi-envelope-arrow-up-fill::before { content: "\f7b9"; }
+.bi-envelope-arrow-up::before { content: "\f7be"; }
+.bi-feather::before { content: "\f7bf"; }
+.bi-feather2::before { content: "\f7c4"; }
+.bi-floppy-fill::before { content: "\f7c5"; }
+.bi-floppy::before { content: "\f7d8"; }
+.bi-floppy2-fill::before { content: "\f7d9"; }
+.bi-floppy2::before { content: "\f7e4"; }
+.bi-gitlab::before { content: "\f7e5"; }
+.bi-highlighter::before { content: "\f7f8"; }
+.bi-marker-tip::before { content: "\f802"; }
+.bi-nvme-fill::before { content: "\f803"; }
+.bi-nvme::before { content: "\f80c"; }
+.bi-opencollective::before { content: "\f80d"; }
+.bi-pci-card-network::before { content: "\f8cd"; }
+.bi-pci-card-sound::before { content: "\f8ce"; }
+.bi-radar::before { content: "\f8cf"; }
+.bi-send-arrow-down-fill::before { content: "\f8d0"; }
+.bi-send-arrow-down::before { content: "\f8d1"; }
+.bi-send-arrow-up-fill::before { content: "\f8d2"; }
+.bi-send-arrow-up::before { content: "\f8d3"; }
+.bi-sim-slash-fill::before { content: "\f8d4"; }
+.bi-sim-slash::before { content: "\f8d5"; }
+.bi-sourceforge::before { content: "\f8d6"; }
+.bi-substack::before { content: "\f8d7"; }
+.bi-threads-fill::before { content: "\f8d8"; }
+.bi-threads::before { content: "\f8d9"; }
+.bi-transparency::before { content: "\f8da"; }
+.bi-twitter-x::before { content: "\f8db"; }
+.bi-type-h4::before { content: "\f8dc"; }
+.bi-type-h5::before { content: "\f8dd"; }
+.bi-type-h6::before { content: "\f8de"; }
+.bi-backpack-fill::before { content: "\f8df"; }
+.bi-backpack::before { content: "\f8e0"; }
+.bi-backpack2-fill::before { content: "\f8e1"; }
+.bi-backpack2::before { content: "\f8e2"; }
+.bi-backpack3-fill::before { content: "\f8e3"; }
+.bi-backpack3::before { content: "\f8e4"; }
+.bi-backpack4-fill::before { content: "\f8e5"; }
+.bi-backpack4::before { content: "\f8e6"; }
+.bi-brilliance::before { content: "\f8e7"; }
+.bi-cake-fill::before { content: "\f8e8"; }
+.bi-cake2-fill::before { content: "\f8e9"; }
+.bi-duffle-fill::before { content: "\f8ea"; }
+.bi-duffle::before { content: "\f8eb"; }
+.bi-exposure::before { content: "\f8ec"; }
+.bi-gender-neuter::before { content: "\f8ed"; }
+.bi-highlights::before { content: "\f8ee"; }
+.bi-luggage-fill::before { content: "\f8ef"; }
+.bi-luggage::before { content: "\f8f0"; }
+.bi-mailbox-flag::before { content: "\f8f1"; }
+.bi-mailbox2-flag::before { content: "\f8f2"; }
+.bi-noise-reduction::before { content: "\f8f3"; }
+.bi-passport-fill::before { content: "\f8f4"; }
+.bi-passport::before { content: "\f8f5"; }
+.bi-person-arms-up::before { content: "\f8f6"; }
+.bi-person-raised-hand::before { content: "\f8f7"; }
+.bi-person-standing-dress::before { content: "\f8f8"; }
+.bi-person-standing::before { content: "\f8f9"; }
+.bi-person-walking::before { content: "\f8fa"; }
+.bi-person-wheelchair::before { content: "\f8fb"; }
+.bi-shadows::before { content: "\f8fc"; }
+.bi-suitcase-fill::before { content: "\f8fd"; }
+.bi-suitcase-lg-fill::before { content: "\f8fe"; }
+.bi-suitcase-lg::before { content: "\f8ff"; }
+.bi-suitcase::before { content: "\f900"; }
+.bi-suitcase2-fill::before { content: "\f901"; }
+.bi-suitcase2::before { content: "\f902"; }
+.bi-vignette::before { content: "\f903"; }
diff --git a/site_libs/bootstrap/bootstrap-icons.woff b/site_libs/bootstrap/bootstrap-icons.woff
new file mode 100644
index 00000000..dbeeb055
Binary files /dev/null and b/site_libs/bootstrap/bootstrap-icons.woff differ
diff --git a/site_libs/bootstrap/bootstrap.min.css b/site_libs/bootstrap/bootstrap.min.css
new file mode 100644
index 00000000..9c5ddbe5
--- /dev/null
+++ b/site_libs/bootstrap/bootstrap.min.css
@@ -0,0 +1,12 @@
+/*!
+ * Bootstrap v5.3.1 (https://getbootstrap.com/)
+ * Copyright 2011-2023 The Bootstrap Authors
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ */@import"https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;700&display=swap";:root,[data-bs-theme=light]{--bs-blue: #2780e3;--bs-indigo: #6610f2;--bs-purple: #613d7c;--bs-pink: #e83e8c;--bs-red: #ff0039;--bs-orange: #f0ad4e;--bs-yellow: #ff7518;--bs-green: #3fb618;--bs-teal: #20c997;--bs-cyan: #9954bb;--bs-black: #000;--bs-white: #fff;--bs-gray: #6c757d;--bs-gray-dark: #343a40;--bs-gray-100: #f8f9fa;--bs-gray-200: #e9ecef;--bs-gray-300: #dee2e6;--bs-gray-400: #ced4da;--bs-gray-500: #adb5bd;--bs-gray-600: #6c757d;--bs-gray-700: #495057;--bs-gray-800: #343a40;--bs-gray-900: #212529;--bs-default: #343a40;--bs-primary: #E8E8FC;--bs-secondary: #343a40;--bs-success: #3fb618;--bs-info: #9954bb;--bs-warning: #ff7518;--bs-danger: #ff0039;--bs-light: #f8f9fa;--bs-dark: #343a40;--bs-default-rgb: 52, 58, 64;--bs-primary-rgb: 232, 232, 252;--bs-secondary-rgb: 52, 58, 64;--bs-success-rgb: 63, 182, 24;--bs-info-rgb: 153, 84, 187;--bs-warning-rgb: 255, 117, 24;--bs-danger-rgb: 255, 0, 57;--bs-light-rgb: 248, 249, 250;--bs-dark-rgb: 52, 58, 64;--bs-primary-text-emphasis: #5d5d65;--bs-secondary-text-emphasis: #15171a;--bs-success-text-emphasis: #19490a;--bs-info-text-emphasis: #3d224b;--bs-warning-text-emphasis: #662f0a;--bs-danger-text-emphasis: #660017;--bs-light-text-emphasis: #495057;--bs-dark-text-emphasis: #495057;--bs-primary-bg-subtle: #fafafe;--bs-secondary-bg-subtle: #d6d8d9;--bs-success-bg-subtle: #d9f0d1;--bs-info-bg-subtle: #ebddf1;--bs-warning-bg-subtle: #ffe3d1;--bs-danger-bg-subtle: #ffccd7;--bs-light-bg-subtle: #fcfcfd;--bs-dark-bg-subtle: #ced4da;--bs-primary-border-subtle: #f6f6fe;--bs-secondary-border-subtle: #aeb0b3;--bs-success-border-subtle: #b2e2a3;--bs-info-border-subtle: #d6bbe4;--bs-warning-border-subtle: #ffc8a3;--bs-danger-border-subtle: #ff99b0;--bs-light-border-subtle: #e9ecef;--bs-dark-border-subtle: #adb5bd;--bs-white-rgb: 255, 255, 255;--bs-black-rgb: 0, 0, 0;--bs-font-sans-serif: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-root-font-size: 18px;--bs-body-font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";--bs-body-font-size:1rem;--bs-body-font-weight: 400;--bs-body-line-height: 1.5;--bs-body-color: #000000;--bs-body-color-rgb: 0, 0, 0;--bs-body-bg: #FFFFFF;--bs-body-bg-rgb: 255, 255, 255;--bs-emphasis-color: #000;--bs-emphasis-color-rgb: 0, 0, 0;--bs-secondary-color: rgba(0, 0, 0, 0.75);--bs-secondary-color-rgb: 0, 0, 0;--bs-secondary-bg: #e9ecef;--bs-secondary-bg-rgb: 233, 236, 239;--bs-tertiary-color: rgba(0, 0, 0, 0.5);--bs-tertiary-color-rgb: 0, 0, 0;--bs-tertiary-bg: #f8f9fa;--bs-tertiary-bg-rgb: 248, 249, 250;--bs-heading-color: inherit;--bs-link-color: #4040BF;--bs-link-color-rgb: 64, 64, 191;--bs-link-decoration: underline;--bs-link-hover-color: #333399;--bs-link-hover-color-rgb: 51, 51, 153;--bs-code-color: #7d12ba;--bs-highlight-bg: #ffe3d1;--bs-border-width: 1px;--bs-border-style: solid;--bs-border-color: #dee2e6;--bs-border-color-translucent: rgba(0, 0, 0, 0.175);--bs-border-radius: 0.25rem;--bs-border-radius-sm: 0.2em;--bs-border-radius-lg: 0.5rem;--bs-border-radius-xl: 1rem;--bs-border-radius-xxl: 2rem;--bs-border-radius-2xl: var(--bs-border-radius-xxl);--bs-border-radius-pill: 50rem;--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width: 0.25rem;--bs-focus-ring-opacity: 0.25;--bs-focus-ring-color: rgba(232, 232, 252, 0.25);--bs-form-valid-color: #3fb618;--bs-form-valid-border-color: #3fb618;--bs-form-invalid-color: #ff0039;--bs-form-invalid-border-color: #ff0039}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color: #dee2e6;--bs-body-color-rgb: 222, 226, 230;--bs-body-bg: #212529;--bs-body-bg-rgb: 33, 37, 41;--bs-emphasis-color: #fff;--bs-emphasis-color-rgb: 255, 255, 255;--bs-secondary-color: rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb: 222, 226, 230;--bs-secondary-bg: #343a40;--bs-secondary-bg-rgb: 52, 58, 64;--bs-tertiary-color: rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb: 222, 226, 230;--bs-tertiary-bg: #2b3035;--bs-tertiary-bg-rgb: 43, 48, 53;--bs-primary-text-emphasis: #f1f1fd;--bs-secondary-text-emphasis: #85898c;--bs-success-text-emphasis: #8cd374;--bs-info-text-emphasis: #c298d6;--bs-warning-text-emphasis: #ffac74;--bs-danger-text-emphasis: #ff6688;--bs-light-text-emphasis: #f8f9fa;--bs-dark-text-emphasis: #dee2e6;--bs-primary-bg-subtle: #2e2e32;--bs-secondary-bg-subtle: #0a0c0d;--bs-success-bg-subtle: #0d2405;--bs-info-bg-subtle: #1f1125;--bs-warning-bg-subtle: #331705;--bs-danger-bg-subtle: #33000b;--bs-light-bg-subtle: #343a40;--bs-dark-bg-subtle: #1a1d20;--bs-primary-border-subtle: #8b8b97;--bs-secondary-border-subtle: #1f2326;--bs-success-border-subtle: #266d0e;--bs-info-border-subtle: #5c3270;--bs-warning-border-subtle: #99460e;--bs-danger-border-subtle: #990022;--bs-light-border-subtle: #495057;--bs-dark-border-subtle: #343a40;--bs-heading-color: inherit;--bs-link-color: #f1f1fd;--bs-link-hover-color: #f4f4fd;--bs-link-color-rgb: 241, 241, 253;--bs-link-hover-color-rgb: 244, 244, 253;--bs-code-color: white;--bs-border-color: #495057;--bs-border-color-translucent: rgba(255, 255, 255, 0.15);--bs-form-valid-color: #8cd374;--bs-form-valid-border-color: #8cd374;--bs-form-invalid-color: #ff6688;--bs-form-invalid-border-color: #ff6688}*,*::before,*::after{box-sizing:border-box}:root{font-size:var(--bs-root-font-size)}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}hr{margin:1rem 0;color:inherit;border:0;border-top:1px solid;opacity:.25}h6,.h6,h5,.h5,h4,.h4,h3,.h3,h2,.h2,h1,.h1{margin-top:0;margin-bottom:.5rem;font-weight:400;line-height:1.2;color:var(--bs-heading-color)}h1,.h1{font-size:calc(1.325rem + 0.9vw)}@media(min-width: 1200px){h1,.h1{font-size:2rem}}h2,.h2{font-size:calc(1.29rem + 0.48vw)}@media(min-width: 1200px){h2,.h2{font-size:1.65rem}}h3,.h3{font-size:calc(1.27rem + 0.24vw)}@media(min-width: 1200px){h3,.h3{font-size:1.45rem}}h4,.h4{font-size:1.25rem}h5,.h5{font-size:1.1rem}h6,.h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{text-decoration:underline dotted;-webkit-text-decoration:underline dotted;-moz-text-decoration:underline dotted;-ms-text-decoration:underline dotted;-o-text-decoration:underline dotted;cursor:help;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}ol,ul,dl{margin-top:0;margin-bottom:1rem}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem;padding:.625rem 1.25rem;border-left:.25rem solid #e9ecef}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}b,strong{font-weight:bolder}small,.small{font-size:0.875em}mark,.mark{padding:.1875em;background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:0.75em;line-height:0;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}a{color:rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));text-decoration:underline;-webkit-text-decoration:underline;-moz-text-decoration:underline;-ms-text-decoration:underline;-o-text-decoration:underline}a:hover{--bs-link-color-rgb: var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}pre,code,kbd,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:0.875em;color:#000;background-color:#f8f9fa;padding:.5rem;border:1px solid var(--bs-border-color, #dee2e6)}pre code{background-color:rgba(0,0,0,0);font-size:inherit;color:inherit;word-break:normal}code{font-size:0.875em;color:var(--bs-code-color);background-color:#f8f9fa;padding:.125rem .25rem;word-wrap:break-word}a>code{color:inherit}kbd{padding:.4rem .4rem;font-size:0.875em;color:#fff;background-color:#000}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:rgba(0,0,0,.75);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}thead,tbody,tfoot,tr,td,th{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}input,button,select,optgroup,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none !important}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button:not(:disabled),[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + 0.3vw);line-height:inherit}@media(min-width: 1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-text,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none !important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:0.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:0.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:0.875em;color:rgba(0,0,0,.75)}.container,.container-fluid,.container-xxl,.container-xl,.container-lg,.container-md,.container-sm{--bs-gutter-x: 1.5rem;--bs-gutter-y: 0;width:100%;padding-right:calc(var(--bs-gutter-x)*.5);padding-left:calc(var(--bs-gutter-x)*.5);margin-right:auto;margin-left:auto}@media(min-width: 576px){.container-sm,.container{max-width:540px}}@media(min-width: 768px){.container-md,.container-sm,.container{max-width:720px}}@media(min-width: 992px){.container-lg,.container-md,.container-sm,.container{max-width:960px}}@media(min-width: 1200px){.container-xl,.container-lg,.container-md,.container-sm,.container{max-width:1140px}}@media(min-width: 1400px){.container-xxl,.container-xl,.container-lg,.container-md,.container-sm,.container{max-width:1320px}}:root{--bs-breakpoint-xs: 0;--bs-breakpoint-sm: 576px;--bs-breakpoint-md: 768px;--bs-breakpoint-lg: 992px;--bs-breakpoint-xl: 1200px;--bs-breakpoint-xxl: 1400px}.grid{display:grid;grid-template-rows:repeat(var(--bs-rows, 1), 1fr);grid-template-columns:repeat(var(--bs-columns, 12), 1fr);gap:var(--bs-gap, 1.5rem)}.grid .g-col-1{grid-column:auto/span 1}.grid .g-col-2{grid-column:auto/span 2}.grid .g-col-3{grid-column:auto/span 3}.grid .g-col-4{grid-column:auto/span 4}.grid .g-col-5{grid-column:auto/span 5}.grid .g-col-6{grid-column:auto/span 6}.grid .g-col-7{grid-column:auto/span 7}.grid .g-col-8{grid-column:auto/span 8}.grid .g-col-9{grid-column:auto/span 9}.grid .g-col-10{grid-column:auto/span 10}.grid .g-col-11{grid-column:auto/span 11}.grid .g-col-12{grid-column:auto/span 12}.grid .g-start-1{grid-column-start:1}.grid .g-start-2{grid-column-start:2}.grid .g-start-3{grid-column-start:3}.grid .g-start-4{grid-column-start:4}.grid .g-start-5{grid-column-start:5}.grid .g-start-6{grid-column-start:6}.grid .g-start-7{grid-column-start:7}.grid .g-start-8{grid-column-start:8}.grid .g-start-9{grid-column-start:9}.grid .g-start-10{grid-column-start:10}.grid .g-start-11{grid-column-start:11}@media(min-width: 576px){.grid .g-col-sm-1{grid-column:auto/span 1}.grid .g-col-sm-2{grid-column:auto/span 2}.grid .g-col-sm-3{grid-column:auto/span 3}.grid .g-col-sm-4{grid-column:auto/span 4}.grid .g-col-sm-5{grid-column:auto/span 5}.grid .g-col-sm-6{grid-column:auto/span 6}.grid .g-col-sm-7{grid-column:auto/span 7}.grid .g-col-sm-8{grid-column:auto/span 8}.grid .g-col-sm-9{grid-column:auto/span 9}.grid .g-col-sm-10{grid-column:auto/span 10}.grid .g-col-sm-11{grid-column:auto/span 11}.grid .g-col-sm-12{grid-column:auto/span 12}.grid .g-start-sm-1{grid-column-start:1}.grid .g-start-sm-2{grid-column-start:2}.grid .g-start-sm-3{grid-column-start:3}.grid .g-start-sm-4{grid-column-start:4}.grid .g-start-sm-5{grid-column-start:5}.grid .g-start-sm-6{grid-column-start:6}.grid .g-start-sm-7{grid-column-start:7}.grid .g-start-sm-8{grid-column-start:8}.grid .g-start-sm-9{grid-column-start:9}.grid .g-start-sm-10{grid-column-start:10}.grid .g-start-sm-11{grid-column-start:11}}@media(min-width: 768px){.grid .g-col-md-1{grid-column:auto/span 1}.grid .g-col-md-2{grid-column:auto/span 2}.grid .g-col-md-3{grid-column:auto/span 3}.grid .g-col-md-4{grid-column:auto/span 4}.grid .g-col-md-5{grid-column:auto/span 5}.grid .g-col-md-6{grid-column:auto/span 6}.grid .g-col-md-7{grid-column:auto/span 7}.grid .g-col-md-8{grid-column:auto/span 8}.grid .g-col-md-9{grid-column:auto/span 9}.grid .g-col-md-10{grid-column:auto/span 10}.grid .g-col-md-11{grid-column:auto/span 11}.grid .g-col-md-12{grid-column:auto/span 12}.grid .g-start-md-1{grid-column-start:1}.grid .g-start-md-2{grid-column-start:2}.grid .g-start-md-3{grid-column-start:3}.grid .g-start-md-4{grid-column-start:4}.grid .g-start-md-5{grid-column-start:5}.grid .g-start-md-6{grid-column-start:6}.grid .g-start-md-7{grid-column-start:7}.grid .g-start-md-8{grid-column-start:8}.grid .g-start-md-9{grid-column-start:9}.grid .g-start-md-10{grid-column-start:10}.grid .g-start-md-11{grid-column-start:11}}@media(min-width: 992px){.grid .g-col-lg-1{grid-column:auto/span 1}.grid .g-col-lg-2{grid-column:auto/span 2}.grid .g-col-lg-3{grid-column:auto/span 3}.grid .g-col-lg-4{grid-column:auto/span 4}.grid .g-col-lg-5{grid-column:auto/span 5}.grid .g-col-lg-6{grid-column:auto/span 6}.grid .g-col-lg-7{grid-column:auto/span 7}.grid .g-col-lg-8{grid-column:auto/span 8}.grid .g-col-lg-9{grid-column:auto/span 9}.grid .g-col-lg-10{grid-column:auto/span 10}.grid .g-col-lg-11{grid-column:auto/span 11}.grid .g-col-lg-12{grid-column:auto/span 12}.grid .g-start-lg-1{grid-column-start:1}.grid .g-start-lg-2{grid-column-start:2}.grid .g-start-lg-3{grid-column-start:3}.grid .g-start-lg-4{grid-column-start:4}.grid .g-start-lg-5{grid-column-start:5}.grid .g-start-lg-6{grid-column-start:6}.grid .g-start-lg-7{grid-column-start:7}.grid .g-start-lg-8{grid-column-start:8}.grid .g-start-lg-9{grid-column-start:9}.grid .g-start-lg-10{grid-column-start:10}.grid .g-start-lg-11{grid-column-start:11}}@media(min-width: 1200px){.grid .g-col-xl-1{grid-column:auto/span 1}.grid .g-col-xl-2{grid-column:auto/span 2}.grid .g-col-xl-3{grid-column:auto/span 3}.grid .g-col-xl-4{grid-column:auto/span 4}.grid .g-col-xl-5{grid-column:auto/span 5}.grid .g-col-xl-6{grid-column:auto/span 6}.grid .g-col-xl-7{grid-column:auto/span 7}.grid .g-col-xl-8{grid-column:auto/span 8}.grid .g-col-xl-9{grid-column:auto/span 9}.grid .g-col-xl-10{grid-column:auto/span 10}.grid .g-col-xl-11{grid-column:auto/span 11}.grid .g-col-xl-12{grid-column:auto/span 12}.grid .g-start-xl-1{grid-column-start:1}.grid .g-start-xl-2{grid-column-start:2}.grid .g-start-xl-3{grid-column-start:3}.grid .g-start-xl-4{grid-column-start:4}.grid .g-start-xl-5{grid-column-start:5}.grid .g-start-xl-6{grid-column-start:6}.grid .g-start-xl-7{grid-column-start:7}.grid .g-start-xl-8{grid-column-start:8}.grid .g-start-xl-9{grid-column-start:9}.grid .g-start-xl-10{grid-column-start:10}.grid .g-start-xl-11{grid-column-start:11}}@media(min-width: 1400px){.grid .g-col-xxl-1{grid-column:auto/span 1}.grid .g-col-xxl-2{grid-column:auto/span 2}.grid .g-col-xxl-3{grid-column:auto/span 3}.grid .g-col-xxl-4{grid-column:auto/span 4}.grid .g-col-xxl-5{grid-column:auto/span 5}.grid .g-col-xxl-6{grid-column:auto/span 6}.grid .g-col-xxl-7{grid-column:auto/span 7}.grid .g-col-xxl-8{grid-column:auto/span 8}.grid .g-col-xxl-9{grid-column:auto/span 9}.grid .g-col-xxl-10{grid-column:auto/span 10}.grid .g-col-xxl-11{grid-column:auto/span 11}.grid .g-col-xxl-12{grid-column:auto/span 12}.grid .g-start-xxl-1{grid-column-start:1}.grid .g-start-xxl-2{grid-column-start:2}.grid .g-start-xxl-3{grid-column-start:3}.grid .g-start-xxl-4{grid-column-start:4}.grid .g-start-xxl-5{grid-column-start:5}.grid .g-start-xxl-6{grid-column-start:6}.grid .g-start-xxl-7{grid-column-start:7}.grid .g-start-xxl-8{grid-column-start:8}.grid .g-start-xxl-9{grid-column-start:9}.grid .g-start-xxl-10{grid-column-start:10}.grid .g-start-xxl-11{grid-column-start:11}}.table{--bs-table-color-type: initial;--bs-table-bg-type: initial;--bs-table-color-state: initial;--bs-table-bg-state: initial;--bs-table-color: #000000;--bs-table-bg: #FFFFFF;--bs-table-border-color: #dee2e6;--bs-table-accent-bg: transparent;--bs-table-striped-color: #000000;--bs-table-striped-bg: rgba(0, 0, 0, 0.05);--bs-table-active-color: #000000;--bs-table-active-bg: rgba(0, 0, 0, 0.1);--bs-table-hover-color: #000000;--bs-table-hover-bg: rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;color:var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state, var(--bs-table-bg-type, var(--bs-table-accent-bg)))}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(1px*2) solid gray}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-color-type: var(--bs-table-striped-color);--bs-table-bg-type: var(--bs-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(even){--bs-table-color-type: var(--bs-table-striped-color);--bs-table-bg-type: var(--bs-table-striped-bg)}.table-active{--bs-table-color-state: var(--bs-table-active-color);--bs-table-bg-state: var(--bs-table-active-bg)}.table-hover>tbody>tr:hover>*{--bs-table-color-state: var(--bs-table-hover-color);--bs-table-bg-state: var(--bs-table-hover-bg)}.table-primary{--bs-table-color: #000;--bs-table-bg: #fafafe;--bs-table-border-color: #e1e1e5;--bs-table-striped-bg: #eeeef1;--bs-table-striped-color: #000;--bs-table-active-bg: #e1e1e5;--bs-table-active-color: #000;--bs-table-hover-bg: #e7e7eb;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color: #000;--bs-table-bg: #d6d8d9;--bs-table-border-color: #c1c2c3;--bs-table-striped-bg: #cbcdce;--bs-table-striped-color: #000;--bs-table-active-bg: #c1c2c3;--bs-table-active-color: #000;--bs-table-hover-bg: #c6c8c9;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color: #000;--bs-table-bg: #d9f0d1;--bs-table-border-color: #c3d8bc;--bs-table-striped-bg: #cee4c7;--bs-table-striped-color: #000;--bs-table-active-bg: #c3d8bc;--bs-table-active-color: #000;--bs-table-hover-bg: #c9dec1;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color: #000;--bs-table-bg: #ebddf1;--bs-table-border-color: #d4c7d9;--bs-table-striped-bg: #dfd2e5;--bs-table-striped-color: #000;--bs-table-active-bg: #d4c7d9;--bs-table-active-color: #000;--bs-table-hover-bg: #d9ccdf;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color: #000;--bs-table-bg: #ffe3d1;--bs-table-border-color: #e6ccbc;--bs-table-striped-bg: #f2d8c7;--bs-table-striped-color: #000;--bs-table-active-bg: #e6ccbc;--bs-table-active-color: #000;--bs-table-hover-bg: #ecd2c1;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color: #000;--bs-table-bg: #ffccd7;--bs-table-border-color: #e6b8c2;--bs-table-striped-bg: #f2c2cc;--bs-table-striped-color: #000;--bs-table-active-bg: #e6b8c2;--bs-table-active-color: #000;--bs-table-hover-bg: #ecbdc7;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color: #000;--bs-table-bg: #f8f9fa;--bs-table-border-color: #dfe0e1;--bs-table-striped-bg: #ecedee;--bs-table-striped-color: #000;--bs-table-active-bg: #dfe0e1;--bs-table-active-color: #000;--bs-table-hover-bg: #e5e6e7;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color: #fff;--bs-table-bg: #343a40;--bs-table-border-color: #484e53;--bs-table-striped-bg: #3e444a;--bs-table-striped-color: #fff;--bs-table-active-bg: #484e53;--bs-table-active-color: #fff;--bs-table-hover-bg: #43494e;--bs-table-hover-color: #fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media(max-width: 575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label,.shiny-input-container .control-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(0.375rem + 1px);padding-bottom:calc(0.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(0.5rem + 1px);padding-bottom:calc(0.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(0.25rem + 1px);padding-bottom:calc(0.25rem + 1px);font-size:0.875rem}.form-text{margin-top:.25rem;font-size:0.875em;color:rgba(0,0,0,.75)}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#000;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none;background-color:#fff;background-clip:padding-box;border:1px solid #dee2e6;border-radius:0;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#000;background-color:#fff;border-color:#f4f4fe;outline:0;box-shadow:0 0 0 .25rem rgba(232,232,252,.25)}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::placeholder{color:rgba(0,0,0,.75);opacity:1}.form-control:disabled{background-color:#e9ecef;opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-0.375rem -0.75rem;margin-inline-end:.75rem;color:#000;background-color:#f8f9fa;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#e9ecef}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#000;background-color:rgba(0,0,0,0);border:solid rgba(0,0,0,0);border-width:1px 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-sm,.form-control-plaintext.form-control-lg{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + 0.5rem + calc(1px * 2));padding:.25rem .5rem;font-size:0.875rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-0.25rem -0.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + calc(1px * 2));padding:.5rem 1rem;font-size:1.25rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-0.5rem -1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + 0.75rem + calc(1px * 2))}textarea.form-control-sm{min-height:calc(1.5em + 0.5rem + calc(1px * 2))}textarea.form-control-lg{min-height:calc(1.5em + 1rem + calc(1px * 2))}.form-control-color{width:3rem;height:calc(1.5em + 0.75rem + calc(1px * 2));padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0 !important}.form-control-color::-webkit-color-swatch{border:0 !important}.form-control-color.form-control-sm{height:calc(1.5em + 0.5rem + calc(1px * 2))}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + calc(1px * 2))}.form-select{--bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#000;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none;background-color:#fff;background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon, none);background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #dee2e6;border-radius:0;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-select{transition:none}}.form-select:focus{border-color:#f4f4fe;outline:0;box-shadow:0 0 0 .25rem rgba(232,232,252,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:rgba(0,0,0,0);text-shadow:0 0 0 #000}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:0.875rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}[data-bs-theme=dark] .form-select{--bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.form-check,.shiny-input-container .checkbox,.shiny-input-container .radio{display:block;min-height:1.5rem;padding-left:0;margin-bottom:.125rem}.form-check .form-check-input,.form-check .shiny-input-container .checkbox input,.form-check .shiny-input-container .radio input,.shiny-input-container .checkbox .form-check-input,.shiny-input-container .checkbox .shiny-input-container .checkbox input,.shiny-input-container .checkbox .shiny-input-container .radio input,.shiny-input-container .radio .form-check-input,.shiny-input-container .radio .shiny-input-container .checkbox input,.shiny-input-container .radio .shiny-input-container .radio input{float:left;margin-left:0}.form-check-reverse{padding-right:0;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:0;margin-left:0}.form-check-input,.shiny-input-container .checkbox input,.shiny-input-container .checkbox-inline input,.shiny-input-container .radio input,.shiny-input-container .radio-inline input{--bs-form-check-bg: #FFFFFF;width:1em;height:1em;margin-top:.25em;vertical-align:top;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid #dee2e6;print-color-adjust:exact}.form-check-input[type=radio],.shiny-input-container .checkbox input[type=radio],.shiny-input-container .checkbox-inline input[type=radio],.shiny-input-container .radio input[type=radio],.shiny-input-container .radio-inline input[type=radio]{border-radius:50%}.form-check-input:active,.shiny-input-container .checkbox input:active,.shiny-input-container .checkbox-inline input:active,.shiny-input-container .radio input:active,.shiny-input-container .radio-inline input:active{filter:brightness(90%)}.form-check-input:focus,.shiny-input-container .checkbox input:focus,.shiny-input-container .checkbox-inline input:focus,.shiny-input-container .radio input:focus,.shiny-input-container .radio-inline input:focus{border-color:#f4f4fe;outline:0;box-shadow:0 0 0 .25rem rgba(232,232,252,.25)}.form-check-input:checked,.shiny-input-container .checkbox input:checked,.shiny-input-container .checkbox-inline input:checked,.shiny-input-container .radio input:checked,.shiny-input-container .radio-inline input:checked{background-color:#e8e8fc;border-color:#e8e8fc}.form-check-input:checked[type=checkbox],.shiny-input-container .checkbox input:checked[type=checkbox],.shiny-input-container .checkbox-inline input:checked[type=checkbox],.shiny-input-container .radio input:checked[type=checkbox],.shiny-input-container .radio-inline input:checked[type=checkbox]{--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio],.shiny-input-container .checkbox input:checked[type=radio],.shiny-input-container .checkbox-inline input:checked[type=radio],.shiny-input-container .radio input:checked[type=radio],.shiny-input-container .radio-inline input:checked[type=radio]{--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23000'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate,.shiny-input-container .checkbox input[type=checkbox]:indeterminate,.shiny-input-container .checkbox-inline input[type=checkbox]:indeterminate,.shiny-input-container .radio input[type=checkbox]:indeterminate,.shiny-input-container .radio-inline input[type=checkbox]:indeterminate{background-color:#e8e8fc;border-color:#e8e8fc;--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled,.shiny-input-container .checkbox input:disabled,.shiny-input-container .checkbox-inline input:disabled,.shiny-input-container .radio input:disabled,.shiny-input-container .radio-inline input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input[disabled]~.form-check-label,.form-check-input[disabled]~span,.form-check-input:disabled~.form-check-label,.form-check-input:disabled~span,.shiny-input-container .checkbox input[disabled]~.form-check-label,.shiny-input-container .checkbox input[disabled]~span,.shiny-input-container .checkbox input:disabled~.form-check-label,.shiny-input-container .checkbox input:disabled~span,.shiny-input-container .checkbox-inline input[disabled]~.form-check-label,.shiny-input-container .checkbox-inline input[disabled]~span,.shiny-input-container .checkbox-inline input:disabled~.form-check-label,.shiny-input-container .checkbox-inline input:disabled~span,.shiny-input-container .radio input[disabled]~.form-check-label,.shiny-input-container .radio input[disabled]~span,.shiny-input-container .radio input:disabled~.form-check-label,.shiny-input-container .radio input:disabled~span,.shiny-input-container .radio-inline input[disabled]~.form-check-label,.shiny-input-container .radio-inline input[disabled]~span,.shiny-input-container .radio-inline input:disabled~.form-check-label,.shiny-input-container .radio-inline input:disabled~span{cursor:default;opacity:.5}.form-check-label,.shiny-input-container .checkbox label,.shiny-input-container .checkbox-inline label,.shiny-input-container .radio label,.shiny-input-container .radio-inline label{cursor:pointer}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);background-position:left center;transition:background-position .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23f4f4fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23000'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.btn-check[disabled]+.btn,.btn-check:disabled+.btn{pointer-events:none;filter:none;opacity:.65}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.5rem;padding:0;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none;background-color:rgba(0,0,0,0)}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(232,232,252,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(232,232,252,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-0.25rem;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none;background-color:#e8e8fc;border:0;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-range::-webkit-slider-thumb{transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#f8f8fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:rgba(0,0,0,0);cursor:pointer;background-color:#f8f9fa;border-color:rgba(0,0,0,0)}.form-range::-moz-range-thumb{width:1rem;height:1rem;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none;background-color:#e8e8fc;border:0;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-range::-moz-range-thumb{transition:none}}.form-range::-moz-range-thumb:active{background-color:#f8f8fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:rgba(0,0,0,0);cursor:pointer;background-color:#f8f9fa;border-color:rgba(0,0,0,0)}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:rgba(0,0,0,.75)}.form-range:disabled::-moz-range-thumb{background-color:rgba(0,0,0,.75)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(1px * 2));min-height:calc(3.5rem + calc(1px * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;height:100%;padding:1rem .75rem;overflow:hidden;text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:1px solid rgba(0,0,0,0);transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media(prefers-reduced-motion: reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control::placeholder,.form-floating>.form-control-plaintext::placeholder{color:rgba(0,0,0,0)}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown),.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill,.form-floating>.form-control-plaintext:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-control-plaintext~label,.form-floating>.form-select~label{color:rgba(var(--bs-body-color-rgb), 0.65);transform:scale(0.85) translateY(-0.5rem) translateX(0.15rem)}.form-floating>.form-control:focus~label::after,.form-floating>.form-control:not(:placeholder-shown)~label::after,.form-floating>.form-control-plaintext~label::after,.form-floating>.form-select~label::after{position:absolute;inset:1rem .375rem;z-index:-1;height:1.5em;content:"";background-color:#fff}.form-floating>.form-control:-webkit-autofill~label{color:rgba(var(--bs-body-color-rgb), 0.65);transform:scale(0.85) translateY(-0.5rem) translateX(0.15rem)}.form-floating>.form-control-plaintext~label{border-width:1px 0}.form-floating>:disabled~label,.form-floating>.form-control:disabled~label{color:#6c757d}.form-floating>:disabled~label::after,.form-floating>.form-control:disabled~label::after{background-color:#e9ecef}.input-group{position:relative;display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;align-items:stretch;-webkit-align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select,.input-group>.form-floating{position:relative;flex:1 1 auto;-webkit-flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus,.input-group>.form-floating:focus-within{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#000;text-align:center;white-space:nowrap;background-color:#f8f9fa;border:1px solid #dee2e6}.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text,.input-group-lg>.btn{padding:.5rem 1rem;font-size:1.25rem}.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text,.input-group-sm>.btn{padding:.25rem .5rem;font-size:0.875rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(1px*-1)}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:0.875em;color:#3fb618}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:0.875rem;color:#fff;background-color:#3fb618}.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip,.is-valid~.valid-feedback,.is-valid~.valid-tooltip{display:block}.was-validated .form-control:valid,.form-control.is-valid{border-color:#3fb618;padding-right:calc(1.5em + 0.75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%233fb618' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.375em + 0.1875rem) center;background-size:calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-control:valid:focus,.form-control.is-valid:focus{border-color:#3fb618;box-shadow:0 0 0 .25rem rgba(63,182,24,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + 0.75rem);background-position:top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem)}.was-validated .form-select:valid,.form-select.is-valid{border-color:#3fb618}.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"],.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"]{--bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%233fb618' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-select:valid:focus,.form-select.is-valid:focus{border-color:#3fb618;box-shadow:0 0 0 .25rem rgba(63,182,24,.25)}.was-validated .form-control-color:valid,.form-control-color.is-valid{width:calc(3rem + calc(1.5em + 0.75rem))}.was-validated .form-check-input:valid,.form-check-input.is-valid{border-color:#3fb618}.was-validated .form-check-input:valid:checked,.form-check-input.is-valid:checked{background-color:#3fb618}.was-validated .form-check-input:valid:focus,.form-check-input.is-valid:focus{box-shadow:0 0 0 .25rem rgba(63,182,24,.25)}.was-validated .form-check-input:valid~.form-check-label,.form-check-input.is-valid~.form-check-label{color:#3fb618}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.was-validated .input-group>.form-control:not(:focus):valid,.input-group>.form-control:not(:focus).is-valid,.was-validated .input-group>.form-select:not(:focus):valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.input-group>.form-floating:not(:focus-within).is-valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:0.875em;color:#ff0039}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:0.875rem;color:#fff;background-color:#ff0039}.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip,.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip{display:block}.was-validated .form-control:invalid,.form-control.is-invalid{border-color:#ff0039;padding-right:calc(1.5em + 0.75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ff0039'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ff0039' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.375em + 0.1875rem) center;background-size:calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-control:invalid:focus,.form-control.is-invalid:focus{border-color:#ff0039;box-shadow:0 0 0 .25rem rgba(255,0,57,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + 0.75rem);background-position:top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem)}.was-validated .form-select:invalid,.form-select.is-invalid{border-color:#ff0039}.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"],.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"]{--bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ff0039'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ff0039' stroke='none'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-select:invalid:focus,.form-select.is-invalid:focus{border-color:#ff0039;box-shadow:0 0 0 .25rem rgba(255,0,57,.25)}.was-validated .form-control-color:invalid,.form-control-color.is-invalid{width:calc(3rem + calc(1.5em + 0.75rem))}.was-validated .form-check-input:invalid,.form-check-input.is-invalid{border-color:#ff0039}.was-validated .form-check-input:invalid:checked,.form-check-input.is-invalid:checked{background-color:#ff0039}.was-validated .form-check-input:invalid:focus,.form-check-input.is-invalid:focus{box-shadow:0 0 0 .25rem rgba(255,0,57,.25)}.was-validated .form-check-input:invalid~.form-check-label,.form-check-input.is-invalid~.form-check-label{color:#ff0039}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.was-validated .input-group>.form-control:not(:focus):invalid,.input-group>.form-control:not(:focus).is-invalid,.was-validated .input-group>.form-select:not(:focus):invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.input-group>.form-floating:not(:focus-within).is-invalid{z-index:4}.btn{--bs-btn-padding-x: 0.75rem;--bs-btn-padding-y: 0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight: 400;--bs-btn-line-height: 1.5;--bs-btn-color: #000000;--bs-btn-bg: transparent;--bs-btn-border-width: 1px;--bs-btn-border-color: transparent;--bs-btn-border-radius: 0.25rem;--bs-btn-hover-border-color: transparent;--bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity: 0.65;--bs-btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;vertical-align:middle;cursor:pointer;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,:not(.btn-check)+.btn:active,.btn:first-child:active,.btn.active,.btn.show{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,:not(.btn-check)+.btn:active:focus-visible,.btn:first-child:active:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn:disabled,.btn.disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-default{--bs-btn-color: #fff;--bs-btn-bg: #343a40;--bs-btn-border-color: #343a40;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #2c3136;--bs-btn-hover-border-color: #2a2e33;--bs-btn-focus-shadow-rgb: 82, 88, 93;--bs-btn-active-color: #fff;--bs-btn-active-bg: #2a2e33;--bs-btn-active-border-color: #272c30;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #343a40;--bs-btn-disabled-border-color: #343a40}.btn-primary{--bs-btn-color: #000;--bs-btn-bg: #E8E8FC;--bs-btn-border-color: #E8E8FC;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #ebebfc;--bs-btn-hover-border-color: #eaeafc;--bs-btn-focus-shadow-rgb: 197, 197, 214;--bs-btn-active-color: #000;--bs-btn-active-bg: #ededfd;--bs-btn-active-border-color: #eaeafc;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #E8E8FC;--bs-btn-disabled-border-color: #E8E8FC}.btn-secondary{--bs-btn-color: #fff;--bs-btn-bg: #343a40;--bs-btn-border-color: #343a40;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #2c3136;--bs-btn-hover-border-color: #2a2e33;--bs-btn-focus-shadow-rgb: 82, 88, 93;--bs-btn-active-color: #fff;--bs-btn-active-bg: #2a2e33;--bs-btn-active-border-color: #272c30;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #343a40;--bs-btn-disabled-border-color: #343a40}.btn-success{--bs-btn-color: #fff;--bs-btn-bg: #3fb618;--bs-btn-border-color: #3fb618;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #369b14;--bs-btn-hover-border-color: #329213;--bs-btn-focus-shadow-rgb: 92, 193, 59;--bs-btn-active-color: #fff;--bs-btn-active-bg: #329213;--bs-btn-active-border-color: #2f8912;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #3fb618;--bs-btn-disabled-border-color: #3fb618}.btn-info{--bs-btn-color: #fff;--bs-btn-bg: #9954bb;--bs-btn-border-color: #9954bb;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #82479f;--bs-btn-hover-border-color: #7a4396;--bs-btn-focus-shadow-rgb: 168, 110, 197;--bs-btn-active-color: #fff;--bs-btn-active-bg: #7a4396;--bs-btn-active-border-color: #733f8c;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #9954bb;--bs-btn-disabled-border-color: #9954bb}.btn-warning{--bs-btn-color: #fff;--bs-btn-bg: #ff7518;--bs-btn-border-color: #ff7518;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #d96314;--bs-btn-hover-border-color: #cc5e13;--bs-btn-focus-shadow-rgb: 255, 138, 59;--bs-btn-active-color: #fff;--bs-btn-active-bg: #cc5e13;--bs-btn-active-border-color: #bf5812;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #ff7518;--bs-btn-disabled-border-color: #ff7518}.btn-danger{--bs-btn-color: #fff;--bs-btn-bg: #ff0039;--bs-btn-border-color: #ff0039;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #d90030;--bs-btn-hover-border-color: #cc002e;--bs-btn-focus-shadow-rgb: 255, 38, 87;--bs-btn-active-color: #fff;--bs-btn-active-bg: #cc002e;--bs-btn-active-border-color: #bf002b;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #ff0039;--bs-btn-disabled-border-color: #ff0039}.btn-light{--bs-btn-color: #000;--bs-btn-bg: #f8f9fa;--bs-btn-border-color: #f8f9fa;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #d3d4d5;--bs-btn-hover-border-color: #c6c7c8;--bs-btn-focus-shadow-rgb: 211, 212, 213;--bs-btn-active-color: #000;--bs-btn-active-bg: #c6c7c8;--bs-btn-active-border-color: #babbbc;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #f8f9fa;--bs-btn-disabled-border-color: #f8f9fa}.btn-dark{--bs-btn-color: #fff;--bs-btn-bg: #343a40;--bs-btn-border-color: #343a40;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #52585d;--bs-btn-hover-border-color: #484e53;--bs-btn-focus-shadow-rgb: 82, 88, 93;--bs-btn-active-color: #fff;--bs-btn-active-bg: #5d6166;--bs-btn-active-border-color: #484e53;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #343a40;--bs-btn-disabled-border-color: #343a40}.btn-outline-default{--bs-btn-color: #343a40;--bs-btn-border-color: #343a40;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #343a40;--bs-btn-hover-border-color: #343a40;--bs-btn-focus-shadow-rgb: 52, 58, 64;--bs-btn-active-color: #fff;--bs-btn-active-bg: #343a40;--bs-btn-active-border-color: #343a40;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #343a40;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #343a40;--bs-btn-bg: transparent;--bs-gradient: none}.btn-outline-primary{--bs-btn-color: #E8E8FC;--bs-btn-border-color: #E8E8FC;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #E8E8FC;--bs-btn-hover-border-color: #E8E8FC;--bs-btn-focus-shadow-rgb: 232, 232, 252;--bs-btn-active-color: #000;--bs-btn-active-bg: #E8E8FC;--bs-btn-active-border-color: #E8E8FC;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #E8E8FC;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #E8E8FC;--bs-btn-bg: transparent;--bs-gradient: none}.btn-outline-secondary{--bs-btn-color: #343a40;--bs-btn-border-color: #343a40;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #343a40;--bs-btn-hover-border-color: #343a40;--bs-btn-focus-shadow-rgb: 52, 58, 64;--bs-btn-active-color: #fff;--bs-btn-active-bg: #343a40;--bs-btn-active-border-color: #343a40;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #343a40;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #343a40;--bs-btn-bg: transparent;--bs-gradient: none}.btn-outline-success{--bs-btn-color: #3fb618;--bs-btn-border-color: #3fb618;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #3fb618;--bs-btn-hover-border-color: #3fb618;--bs-btn-focus-shadow-rgb: 63, 182, 24;--bs-btn-active-color: #fff;--bs-btn-active-bg: #3fb618;--bs-btn-active-border-color: #3fb618;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #3fb618;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #3fb618;--bs-btn-bg: transparent;--bs-gradient: none}.btn-outline-info{--bs-btn-color: #9954bb;--bs-btn-border-color: #9954bb;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #9954bb;--bs-btn-hover-border-color: #9954bb;--bs-btn-focus-shadow-rgb: 153, 84, 187;--bs-btn-active-color: #fff;--bs-btn-active-bg: #9954bb;--bs-btn-active-border-color: #9954bb;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #9954bb;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #9954bb;--bs-btn-bg: transparent;--bs-gradient: none}.btn-outline-warning{--bs-btn-color: #ff7518;--bs-btn-border-color: #ff7518;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #ff7518;--bs-btn-hover-border-color: #ff7518;--bs-btn-focus-shadow-rgb: 255, 117, 24;--bs-btn-active-color: #fff;--bs-btn-active-bg: #ff7518;--bs-btn-active-border-color: #ff7518;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #ff7518;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #ff7518;--bs-btn-bg: transparent;--bs-gradient: none}.btn-outline-danger{--bs-btn-color: #ff0039;--bs-btn-border-color: #ff0039;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #ff0039;--bs-btn-hover-border-color: #ff0039;--bs-btn-focus-shadow-rgb: 255, 0, 57;--bs-btn-active-color: #fff;--bs-btn-active-bg: #ff0039;--bs-btn-active-border-color: #ff0039;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #ff0039;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #ff0039;--bs-btn-bg: transparent;--bs-gradient: none}.btn-outline-light{--bs-btn-color: #f8f9fa;--bs-btn-border-color: #f8f9fa;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #f8f9fa;--bs-btn-hover-border-color: #f8f9fa;--bs-btn-focus-shadow-rgb: 248, 249, 250;--bs-btn-active-color: #000;--bs-btn-active-bg: #f8f9fa;--bs-btn-active-border-color: #f8f9fa;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #f8f9fa;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #f8f9fa;--bs-btn-bg: transparent;--bs-gradient: none}.btn-outline-dark{--bs-btn-color: #343a40;--bs-btn-border-color: #343a40;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #343a40;--bs-btn-hover-border-color: #343a40;--bs-btn-focus-shadow-rgb: 52, 58, 64;--bs-btn-active-color: #fff;--bs-btn-active-bg: #343a40;--bs-btn-active-border-color: #343a40;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #343a40;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #343a40;--bs-btn-bg: transparent;--bs-gradient: none}.btn-link{--bs-btn-font-weight: 400;--bs-btn-color: #4040BF;--bs-btn-bg: transparent;--bs-btn-border-color: transparent;--bs-btn-hover-color: #333399;--bs-btn-hover-border-color: transparent;--bs-btn-active-color: #333399;--bs-btn-active-border-color: transparent;--bs-btn-disabled-color: #6c757d;--bs-btn-disabled-border-color: transparent;--bs-btn-box-shadow: 0 0 0 #000;--bs-btn-focus-shadow-rgb: 93, 93, 201;text-decoration:underline;-webkit-text-decoration:underline;-moz-text-decoration:underline;-ms-text-decoration:underline;-o-text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-lg,.btn-group-lg>.btn{--bs-btn-padding-y: 0.5rem;--bs-btn-padding-x: 1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius: 0.5rem}.btn-sm,.btn-group-sm>.btn{--bs-btn-padding-y: 0.25rem;--bs-btn-padding-x: 0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius: 0.2em}.fade{transition:opacity .15s linear}@media(prefers-reduced-motion: reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .2s ease}@media(prefers-reduced-motion: reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media(prefers-reduced-motion: reduce){.collapsing.collapse-horizontal{transition:none}}.dropup,.dropend,.dropdown,.dropstart,.dropup-center,.dropdown-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid rgba(0,0,0,0);border-bottom:0;border-left:.3em solid rgba(0,0,0,0)}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex: 1000;--bs-dropdown-min-width: 10rem;--bs-dropdown-padding-x: 0;--bs-dropdown-padding-y: 0.5rem;--bs-dropdown-spacer: 0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color: #000000;--bs-dropdown-bg: #FFFFFF;--bs-dropdown-border-color: rgba(0, 0, 0, 0.175);--bs-dropdown-border-radius: 0.25rem;--bs-dropdown-border-width: 1px;--bs-dropdown-inner-border-radius: calc(0.25rem - 1px);--bs-dropdown-divider-bg: rgba(0, 0, 0, 0.175);--bs-dropdown-divider-margin-y: 0.5rem;--bs-dropdown-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-dropdown-link-color: #000000;--bs-dropdown-link-hover-color: #000000;--bs-dropdown-link-hover-bg: #f8f9fa;--bs-dropdown-link-active-color: #000;--bs-dropdown-link-active-bg: #E8E8FC;--bs-dropdown-link-disabled-color: rgba(0, 0, 0, 0.5);--bs-dropdown-item-padding-x: 1rem;--bs-dropdown-item-padding-y: 0.25rem;--bs-dropdown-header-color: #6c757d;--bs-dropdown-header-padding-x: 1rem;--bs-dropdown-header-padding-y: 0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position: start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position: end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media(min-width: 576px){.dropdown-menu-sm-start{--bs-position: start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position: end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 768px){.dropdown-menu-md-start{--bs-position: start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position: end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 992px){.dropdown-menu-lg-start{--bs-position: start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position: end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 1200px){.dropdown-menu-xl-start{--bs-position: start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position: end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 1400px){.dropdown-menu-xxl-start{--bs-position: start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position: end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid rgba(0,0,0,0);border-bottom:.3em solid;border-left:.3em solid rgba(0,0,0,0)}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid rgba(0,0,0,0);border-right:0;border-bottom:.3em solid rgba(0,0,0,0);border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid rgba(0,0,0,0);border-right:.3em solid;border-bottom:.3em solid rgba(0,0,0,0)}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;white-space:nowrap;background-color:rgba(0,0,0,0);border:0}.dropdown-item:hover,.dropdown-item:focus{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:rgba(0,0,0,0)}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:0.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color: #dee2e6;--bs-dropdown-bg: #343a40;--bs-dropdown-border-color: rgba(0, 0, 0, 0.175);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color: #dee2e6;--bs-dropdown-link-hover-color: #fff;--bs-dropdown-divider-bg: rgba(0, 0, 0, 0.175);--bs-dropdown-link-hover-bg: rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color: #000;--bs-dropdown-link-active-bg: #E8E8FC;--bs-dropdown-link-disabled-color: #adb5bd;--bs-dropdown-header-color: #adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;flex:1 1 auto;-webkit-flex:1 1 auto}.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn:hover,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:1}.btn-toolbar{display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;justify-content:flex-start;-webkit-justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>:not(.btn-check:first-child)+.btn,.btn-group>.btn-group:not(:first-child){margin-left:calc(1px*-1)}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-sm+.dropdown-toggle-split,.btn-group-sm>.btn+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-lg+.dropdown-toggle-split,.btn-group-lg>.btn+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;-webkit-flex-direction:column;align-items:flex-start;-webkit-align-items:flex-start;justify-content:center;-webkit-justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child){margin-top:calc(1px*-1)}.nav{--bs-nav-link-padding-x: 1rem;--bs-nav-link-padding-y: 0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color: #4040BF;--bs-nav-link-hover-color: #333399;--bs-nav-link-disabled-color: rgba(0, 0, 0, 0.75);display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;background:none;border:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media(prefers-reduced-motion: reduce){.nav-link{transition:none}}.nav-link:hover,.nav-link:focus{color:var(--bs-nav-link-hover-color)}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(232,232,252,.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width: 1px;--bs-nav-tabs-border-color: #dee2e6;--bs-nav-tabs-border-radius: 0.25rem;--bs-nav-tabs-link-hover-border-color: #e9ecef #e9ecef #dee2e6;--bs-nav-tabs-link-active-color: #000;--bs-nav-tabs-link-active-bg: #FFFFFF;--bs-nav-tabs-link-active-border-color: #dee2e6 #dee2e6 #FFFFFF;border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1*var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width) solid rgba(0,0,0,0)}.nav-tabs .nav-link:hover,.nav-tabs .nav-link:focus{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-link.active,.nav-tabs .nav-item.show .nav-link{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1*var(--bs-nav-tabs-border-width))}.nav-pills{--bs-nav-pills-border-radius: 0.25rem;--bs-nav-pills-link-active-color: #000;--bs-nav-pills-link-active-bg: #E8E8FC}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-underline{--bs-nav-underline-gap: 1rem;--bs-nav-underline-border-width: 0.125rem;--bs-nav-underline-link-active-color: #000;gap:var(--bs-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--bs-nav-underline-border-width) solid rgba(0,0,0,0)}.nav-underline .nav-link:hover,.nav-underline .nav-link:focus{border-bottom-color:currentcolor}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:700;color:var(--bs-nav-underline-link-active-color);border-bottom-color:currentcolor}.nav-fill>.nav-link,.nav-fill .nav-item{flex:1 1 auto;-webkit-flex:1 1 auto;text-align:center}.nav-justified>.nav-link,.nav-justified .nav-item{flex-basis:0;-webkit-flex-basis:0;flex-grow:1;-webkit-flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x: 0;--bs-navbar-padding-y: 0.5rem;--bs-navbar-color: #FFFFFF;--bs-navbar-hover-color: rgba(0, 0, 0, 0.8);--bs-navbar-disabled-color: rgba(255, 255, 255, 0.75);--bs-navbar-active-color: #000000;--bs-navbar-brand-padding-y: 0.3125rem;--bs-navbar-brand-margin-end: 1rem;--bs-navbar-brand-font-size: 1.25rem;--bs-navbar-brand-color: #FFFFFF;--bs-navbar-brand-hover-color: #000000;--bs-navbar-nav-link-padding-x: 0.5rem;--bs-navbar-toggler-padding-y: 0.25;--bs-navbar-toggler-padding-x: 0;--bs-navbar-toggler-font-size: 1.25rem;--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%23FFFFFF' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color: rgba(255, 255, 255, 0);--bs-navbar-toggler-border-radius: 0.25rem;--bs-navbar-toggler-focus-width: 0.25rem;--bs-navbar-toggler-transition: box-shadow 0.15s ease-in-out;position:relative;display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;align-items:center;-webkit-align-items:center;justify-content:space-between;-webkit-justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-sm,.navbar>.container-md,.navbar>.container-lg,.navbar>.container-xl,.navbar>.container-xxl{display:flex;display:-webkit-flex;flex-wrap:inherit;-webkit-flex-wrap:inherit;align-items:center;-webkit-align-items:center;justify-content:space-between;-webkit-justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;white-space:nowrap}.navbar-brand:hover,.navbar-brand:focus{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x: 0;--bs-nav-link-padding-y: 0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color: var(--bs-navbar-color);--bs-nav-link-hover-color: var(--bs-navbar-hover-color);--bs-nav-link-disabled-color: var(--bs-navbar-disabled-color);display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:hover,.navbar-text a:focus{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-basis:100%;-webkit-flex-basis:100%;flex-grow:1;-webkit-flex-grow:1;align-items:center;-webkit-align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:rgba(0,0,0,0);border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);transition:var(--bs-navbar-toggler-transition)}@media(prefers-reduced-motion: reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height, 75vh);overflow-y:auto}@media(min-width: 576px){.navbar-expand-sm{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;-webkit-flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 768px){.navbar-expand-md{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;-webkit-flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 992px){.navbar-expand-lg{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;-webkit-flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 1200px){.navbar-expand-xl{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;-webkit-flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 1400px){.navbar-expand-xxl{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;-webkit-flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;-webkit-flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}.navbar-dark,.navbar[data-bs-theme=dark]{--bs-navbar-color: #FFFFFF;--bs-navbar-hover-color: rgba(0, 0, 0, 0.8);--bs-navbar-disabled-color: rgba(255, 255, 255, 0.75);--bs-navbar-active-color: #000000;--bs-navbar-brand-color: #FFFFFF;--bs-navbar-brand-hover-color: #000000;--bs-navbar-toggler-border-color: rgba(255, 255, 255, 0);--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%23FFFFFF' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}[data-bs-theme=dark] .navbar-toggler-icon{--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%23FFFFFF' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y: 1rem;--bs-card-spacer-x: 1rem;--bs-card-title-spacer-y: 0.5rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width: 1px;--bs-card-border-color: rgba(0, 0, 0, 0.175);--bs-card-border-radius: 0.25rem;--bs-card-box-shadow: ;--bs-card-inner-border-radius: calc(0.25rem - 1px);--bs-card-cap-padding-y: 0.5rem;--bs-card-cap-padding-x: 1rem;--bs-card-cap-bg: rgba(52, 58, 64, 0.25);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg: #FFFFFF;--bs-card-img-overlay-padding: 1rem;--bs-card-group-margin: 0.75rem;position:relative;display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0}.card>.list-group:last-child{border-bottom-width:0}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;-webkit-flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-0.5*var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header-tabs{margin-right:calc(-0.5*var(--bs-card-cap-padding-x));margin-bottom:calc(-1*var(--bs-card-cap-padding-y));margin-left:calc(-0.5*var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-0.5*var(--bs-card-cap-padding-x));margin-left:calc(-0.5*var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding)}.card-img,.card-img-top,.card-img-bottom{width:100%}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media(min-width: 576px){.card-group{display:flex;display:-webkit-flex;flex-flow:row wrap;-webkit-flex-flow:row wrap}.card-group>.card{flex:1 0 0%;-webkit-flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}}.accordion{--bs-accordion-color: #000000;--bs-accordion-bg: #FFFFFF;--bs-accordion-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease;--bs-accordion-border-color: #dee2e6;--bs-accordion-border-width: 1px;--bs-accordion-border-radius: 0.25rem;--bs-accordion-inner-border-radius: calc(0.25rem - 1px);--bs-accordion-btn-padding-x: 1.25rem;--bs-accordion-btn-padding-y: 1rem;--bs-accordion-btn-color: #000000;--bs-accordion-btn-bg: #FFFFFF;--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000000'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width: 1.25rem;--bs-accordion-btn-icon-transform: rotate(-180deg);--bs-accordion-btn-icon-transition: transform 0.2s ease-in-out;--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%235d5d65'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-focus-border-color: #f4f4fe;--bs-accordion-btn-focus-box-shadow: 0 0 0 0.25rem rgba(232, 232, 252, 0.25);--bs-accordion-body-padding-x: 1.25rem;--bs-accordion-body-padding-y: 1rem;--bs-accordion-active-color: #5d5d65;--bs-accordion-active-bg: #fafafe}.accordion-button{position:relative;display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media(prefers-reduced-motion: reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1*var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;-webkit-flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media(prefers-reduced-motion: reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:var(--bs-accordion-btn-focus-border-color);outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:not(:first-of-type){border-top:0}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23f1f1fd'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23f1f1fd'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.breadcrumb{--bs-breadcrumb-padding-x: 0;--bs-breadcrumb-padding-y: 0;--bs-breadcrumb-margin-bottom: 1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color: rgba(0, 0, 0, 0.75);--bs-breadcrumb-item-padding-x: 0.5rem;--bs-breadcrumb-item-active-color: rgba(0, 0, 0, 0.75);display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, ">") /* rtl: var(--bs-breadcrumb-divider, ">") */}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x: 0.75rem;--bs-pagination-padding-y: 0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color: #4040BF;--bs-pagination-bg: #FFFFFF;--bs-pagination-border-width: 1px;--bs-pagination-border-color: #dee2e6;--bs-pagination-border-radius: 0.25rem;--bs-pagination-hover-color: #333399;--bs-pagination-hover-bg: #f8f9fa;--bs-pagination-hover-border-color: #dee2e6;--bs-pagination-focus-color: #333399;--bs-pagination-focus-bg: #e9ecef;--bs-pagination-focus-box-shadow: 0 0 0 0.25rem rgba(232, 232, 252, 0.25);--bs-pagination-active-color: #000;--bs-pagination-active-bg: #E8E8FC;--bs-pagination-active-border-color: #E8E8FC;--bs-pagination-disabled-color: rgba(0, 0, 0, 0.75);--bs-pagination-disabled-bg: #e9ecef;--bs-pagination-disabled-border-color: #dee2e6;display:flex;display:-webkit-flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.page-link.active,.active>.page-link{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.page-link.disabled,.disabled>.page-link{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(1px*-1)}.pagination-lg{--bs-pagination-padding-x: 1.5rem;--bs-pagination-padding-y: 0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius: 0.5rem}.pagination-sm{--bs-pagination-padding-x: 0.5rem;--bs-pagination-padding-y: 0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius: 0.2em}.badge{--bs-badge-padding-x: 0.65em;--bs-badge-padding-y: 0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight: 700;--bs-badge-color: #fff;--bs-badge-border-radius: 0.25rem;display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg: transparent;--bs-alert-padding-x: 1rem;--bs-alert-padding-y: 1rem;--bs-alert-margin-bottom: 1rem;--bs-alert-color: inherit;--bs-alert-border-color: transparent;--bs-alert-border: 0 solid var(--bs-alert-border-color);--bs-alert-border-radius: 0.25rem;--bs-alert-link-color: inherit;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border)}.alert-heading{color:inherit}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-default{--bs-alert-color: var(--bs-default-text-emphasis);--bs-alert-bg: var(--bs-default-bg-subtle);--bs-alert-border-color: var(--bs-default-border-subtle);--bs-alert-link-color: var(--bs-default-text-emphasis)}.alert-primary{--bs-alert-color: var(--bs-primary-text-emphasis);--bs-alert-bg: var(--bs-primary-bg-subtle);--bs-alert-border-color: var(--bs-primary-border-subtle);--bs-alert-link-color: var(--bs-primary-text-emphasis)}.alert-secondary{--bs-alert-color: var(--bs-secondary-text-emphasis);--bs-alert-bg: var(--bs-secondary-bg-subtle);--bs-alert-border-color: var(--bs-secondary-border-subtle);--bs-alert-link-color: var(--bs-secondary-text-emphasis)}.alert-success{--bs-alert-color: var(--bs-success-text-emphasis);--bs-alert-bg: var(--bs-success-bg-subtle);--bs-alert-border-color: var(--bs-success-border-subtle);--bs-alert-link-color: var(--bs-success-text-emphasis)}.alert-info{--bs-alert-color: var(--bs-info-text-emphasis);--bs-alert-bg: var(--bs-info-bg-subtle);--bs-alert-border-color: var(--bs-info-border-subtle);--bs-alert-link-color: var(--bs-info-text-emphasis)}.alert-warning{--bs-alert-color: var(--bs-warning-text-emphasis);--bs-alert-bg: var(--bs-warning-bg-subtle);--bs-alert-border-color: var(--bs-warning-border-subtle);--bs-alert-link-color: var(--bs-warning-text-emphasis)}.alert-danger{--bs-alert-color: var(--bs-danger-text-emphasis);--bs-alert-bg: var(--bs-danger-bg-subtle);--bs-alert-border-color: var(--bs-danger-border-subtle);--bs-alert-link-color: var(--bs-danger-text-emphasis)}.alert-light{--bs-alert-color: var(--bs-light-text-emphasis);--bs-alert-bg: var(--bs-light-bg-subtle);--bs-alert-border-color: var(--bs-light-border-subtle);--bs-alert-link-color: var(--bs-light-text-emphasis)}.alert-dark{--bs-alert-color: var(--bs-dark-text-emphasis);--bs-alert-bg: var(--bs-dark-bg-subtle);--bs-alert-border-color: var(--bs-dark-border-subtle);--bs-alert-link-color: var(--bs-dark-text-emphasis)}@keyframes progress-bar-stripes{0%{background-position-x:.5rem}}.progress,.progress-stacked{--bs-progress-height: 0.5rem;--bs-progress-font-size:0.75rem;--bs-progress-bg: #e9ecef;--bs-progress-border-radius: 0.25rem;--bs-progress-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-progress-bar-color: #fff;--bs-progress-bar-bg: #E8E8FC;--bs-progress-bar-transition: width 0.6s ease;display:flex;display:-webkit-flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg)}.progress-bar{display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;justify-content:center;-webkit-justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media(prefers-reduced-motion: reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media(prefers-reduced-motion: reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color: #000000;--bs-list-group-bg: #FFFFFF;--bs-list-group-border-color: #dee2e6;--bs-list-group-border-width: 1px;--bs-list-group-border-radius: 0.25rem;--bs-list-group-item-padding-x: 1rem;--bs-list-group-item-padding-y: 0.5rem;--bs-list-group-action-color: rgba(0, 0, 0, 0.75);--bs-list-group-action-hover-color: #000;--bs-list-group-action-hover-bg: #f8f9fa;--bs-list-group-action-active-color: #000000;--bs-list-group-action-active-bg: #e9ecef;--bs-list-group-disabled-color: rgba(0, 0, 0, 0.75);--bs-list-group-disabled-bg: #FFFFFF;--bs-list-group-active-color: #000;--bs-list-group-active-bg: #E8E8FC;--bs-list-group-active-border-color: #E8E8FC;display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;padding-left:0;margin-bottom:0}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:hover,.list-group-item-action:focus{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1*var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media(min-width: 576px){.list-group-horizontal-sm{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width: 768px){.list-group-horizontal-md{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width: 992px){.list-group-horizontal-lg{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width: 1200px){.list-group-horizontal-xl{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width: 1400px){.list-group-horizontal-xxl{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-default{--bs-list-group-color: var(--bs-default-text-emphasis);--bs-list-group-bg: var(--bs-default-bg-subtle);--bs-list-group-border-color: var(--bs-default-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-default-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-default-border-subtle);--bs-list-group-active-color: var(--bs-default-bg-subtle);--bs-list-group-active-bg: var(--bs-default-text-emphasis);--bs-list-group-active-border-color: var(--bs-default-text-emphasis)}.list-group-item-primary{--bs-list-group-color: var(--bs-primary-text-emphasis);--bs-list-group-bg: var(--bs-primary-bg-subtle);--bs-list-group-border-color: var(--bs-primary-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-primary-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-primary-border-subtle);--bs-list-group-active-color: var(--bs-primary-bg-subtle);--bs-list-group-active-bg: var(--bs-primary-text-emphasis);--bs-list-group-active-border-color: var(--bs-primary-text-emphasis)}.list-group-item-secondary{--bs-list-group-color: var(--bs-secondary-text-emphasis);--bs-list-group-bg: var(--bs-secondary-bg-subtle);--bs-list-group-border-color: var(--bs-secondary-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-secondary-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-secondary-border-subtle);--bs-list-group-active-color: var(--bs-secondary-bg-subtle);--bs-list-group-active-bg: var(--bs-secondary-text-emphasis);--bs-list-group-active-border-color: var(--bs-secondary-text-emphasis)}.list-group-item-success{--bs-list-group-color: var(--bs-success-text-emphasis);--bs-list-group-bg: var(--bs-success-bg-subtle);--bs-list-group-border-color: var(--bs-success-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-success-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-success-border-subtle);--bs-list-group-active-color: var(--bs-success-bg-subtle);--bs-list-group-active-bg: var(--bs-success-text-emphasis);--bs-list-group-active-border-color: var(--bs-success-text-emphasis)}.list-group-item-info{--bs-list-group-color: var(--bs-info-text-emphasis);--bs-list-group-bg: var(--bs-info-bg-subtle);--bs-list-group-border-color: var(--bs-info-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-info-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-info-border-subtle);--bs-list-group-active-color: var(--bs-info-bg-subtle);--bs-list-group-active-bg: var(--bs-info-text-emphasis);--bs-list-group-active-border-color: var(--bs-info-text-emphasis)}.list-group-item-warning{--bs-list-group-color: var(--bs-warning-text-emphasis);--bs-list-group-bg: var(--bs-warning-bg-subtle);--bs-list-group-border-color: var(--bs-warning-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-warning-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-warning-border-subtle);--bs-list-group-active-color: var(--bs-warning-bg-subtle);--bs-list-group-active-bg: var(--bs-warning-text-emphasis);--bs-list-group-active-border-color: var(--bs-warning-text-emphasis)}.list-group-item-danger{--bs-list-group-color: var(--bs-danger-text-emphasis);--bs-list-group-bg: var(--bs-danger-bg-subtle);--bs-list-group-border-color: var(--bs-danger-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-danger-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-danger-border-subtle);--bs-list-group-active-color: var(--bs-danger-bg-subtle);--bs-list-group-active-bg: var(--bs-danger-text-emphasis);--bs-list-group-active-border-color: var(--bs-danger-text-emphasis)}.list-group-item-light{--bs-list-group-color: var(--bs-light-text-emphasis);--bs-list-group-bg: var(--bs-light-bg-subtle);--bs-list-group-border-color: var(--bs-light-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-light-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-light-border-subtle);--bs-list-group-active-color: var(--bs-light-bg-subtle);--bs-list-group-active-bg: var(--bs-light-text-emphasis);--bs-list-group-active-border-color: var(--bs-light-text-emphasis)}.list-group-item-dark{--bs-list-group-color: var(--bs-dark-text-emphasis);--bs-list-group-bg: var(--bs-dark-bg-subtle);--bs-list-group-border-color: var(--bs-dark-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-dark-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-dark-border-subtle);--bs-list-group-active-color: var(--bs-dark-bg-subtle);--bs-list-group-active-bg: var(--bs-dark-text-emphasis);--bs-list-group-active-border-color: var(--bs-dark-text-emphasis)}.btn-close{--bs-btn-close-color: #000;--bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");--bs-btn-close-opacity: 0.5;--bs-btn-close-hover-opacity: 0.75;--bs-btn-close-focus-shadow: 0 0 0 0.25rem rgba(232, 232, 252, 0.25);--bs-btn-close-focus-opacity: 1;--bs-btn-close-disabled-opacity: 0.25;--bs-btn-close-white-filter: invert(1) grayscale(100%) brightness(200%);box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:var(--bs-btn-close-color);background:rgba(0,0,0,0) var(--bs-btn-close-bg) center/1em auto no-repeat;border:0;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close:disabled,.btn-close.disabled{pointer-events:none;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white{filter:var(--bs-btn-close-white-filter)}[data-bs-theme=dark] .btn-close{filter:var(--bs-btn-close-white-filter)}.toast{--bs-toast-zindex: 1090;--bs-toast-padding-x: 0.75rem;--bs-toast-padding-y: 0.5rem;--bs-toast-spacing: 1.5rem;--bs-toast-max-width: 350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg: rgba(255, 255, 255, 0.85);--bs-toast-border-width: 1px;--bs-toast-border-color: rgba(0, 0, 0, 0.175);--bs-toast-border-radius: 0.25rem;--bs-toast-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-toast-header-color: rgba(0, 0, 0, 0.75);--bs-toast-header-bg: rgba(255, 255, 255, 0.85);--bs-toast-header-border-color: rgba(0, 0, 0, 0.175);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex: 1090;position:absolute;z-index:var(--bs-toast-zindex);width:max-content;width:-webkit-max-content;width:-moz-max-content;width:-ms-max-content;width:-o-max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color)}.toast-header .btn-close{margin-right:calc(-0.5*var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex: 1055;--bs-modal-width: 500px;--bs-modal-padding: 1rem;--bs-modal-margin: 0.5rem;--bs-modal-color: ;--bs-modal-bg: #FFFFFF;--bs-modal-border-color: rgba(0, 0, 0, 0.175);--bs-modal-border-width: 1px;--bs-modal-border-radius: 0.5rem;--bs-modal-box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-modal-inner-border-radius: calc(0.5rem - 1px);--bs-modal-header-padding-x: 1rem;--bs-modal-header-padding-y: 1rem;--bs-modal-header-padding: 1rem 1rem;--bs-modal-header-border-color: #dee2e6;--bs-modal-header-border-width: 1px;--bs-modal-title-line-height: 1.5;--bs-modal-footer-gap: 0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color: #dee2e6;--bs-modal-footer-border-width: 1px;position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0, -50px)}@media(prefers-reduced-motion: reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin)*2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;min-height:calc(100% - var(--bs-modal-margin)*2)}.modal-content{position:relative;display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);outline:0}.modal-backdrop{--bs-backdrop-zindex: 1050;--bs-backdrop-bg: #000;--bs-backdrop-opacity: 0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;display:-webkit-flex;flex-shrink:0;-webkit-flex-shrink:0;align-items:center;-webkit-align-items:center;justify-content:space-between;-webkit-justify-content:space-between;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y)*.5) calc(var(--bs-modal-header-padding-x)*.5);margin:calc(-0.5*var(--bs-modal-header-padding-y)) calc(-0.5*var(--bs-modal-header-padding-x)) calc(-0.5*var(--bs-modal-header-padding-y)) auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;-webkit-flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;display:-webkit-flex;flex-shrink:0;-webkit-flex-shrink:0;flex-wrap:wrap;-webkit-flex-wrap:wrap;align-items:center;-webkit-align-items:center;justify-content:flex-end;-webkit-justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap)*.5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap)*.5)}@media(min-width: 576px){.modal{--bs-modal-margin: 1.75rem;--bs-modal-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width: 300px}}@media(min-width: 992px){.modal-lg,.modal-xl{--bs-modal-width: 800px}}@media(min-width: 1200px){.modal-xl{--bs-modal-width: 1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0}.modal-fullscreen .modal-body{overflow-y:auto}@media(max-width: 575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media(max-width: 767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media(max-width: 991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media(max-width: 1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media(max-width: 1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex: 1080;--bs-tooltip-max-width: 200px;--bs-tooltip-padding-x: 0.5rem;--bs-tooltip-padding-y: 0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color: #FFFFFF;--bs-tooltip-bg: #000;--bs-tooltip-border-radius: 0.25rem;--bs-tooltip-opacity: 0.9;--bs-tooltip-arrow-width: 0.8rem;--bs-tooltip-arrow-height: 0.4rem;z-index:var(--bs-tooltip-zindex);display:block;margin:var(--bs-tooltip-margin);font-family:"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:rgba(0,0,0,0);border-style:solid}.bs-tooltip-top .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow{bottom:calc(-1*var(--bs-tooltip-arrow-height))}.bs-tooltip-top .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width)*.5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-end .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow{left:calc(-1*var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-end .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width)*.5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width)*.5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-bottom .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow{top:calc(-1*var(--bs-tooltip-arrow-height))}.bs-tooltip-bottom .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width)*.5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-start .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow{right:calc(-1*var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-start .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width)*.5) 0 calc(var(--bs-tooltip-arrow-width)*.5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg)}.popover{--bs-popover-zindex: 1070;--bs-popover-max-width: 276px;--bs-popover-font-size:0.875rem;--bs-popover-bg: #FFFFFF;--bs-popover-border-width: 1px;--bs-popover-border-color: rgba(0, 0, 0, 0.175);--bs-popover-border-radius: 0.5rem;--bs-popover-inner-border-radius: calc(0.5rem - 1px);--bs-popover-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-popover-header-padding-x: 1rem;--bs-popover-header-padding-y: 0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color: inherit;--bs-popover-header-bg: #e9ecef;--bs-popover-body-padding-x: 1rem;--bs-popover-body-padding-y: 1rem;--bs-popover-body-color: #000000;--bs-popover-arrow-width: 1rem;--bs-popover-arrow-height: 0.5rem;--bs-popover-arrow-border: var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::before,.popover .popover-arrow::after{position:absolute;display:block;content:"";border-color:rgba(0,0,0,0);border-style:solid;border-width:0}.bs-popover-top>.popover-arrow,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow{bottom:calc(-1*(var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-top>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width)*.5) 0}.bs-popover-top>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-top>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-end>.popover-arrow,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow{left:calc(-1*(var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-end>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after{border-width:calc(var(--bs-popover-arrow-width)*.5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width)*.5) 0}.bs-popover-end>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-end>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-bottom>.popover-arrow,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow{top:calc(-1*(var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-bottom>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after{border-width:0 calc(var(--bs-popover-arrow-width)*.5) var(--bs-popover-arrow-height)}.bs-popover-bottom>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-bottom>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-bottom .popover-header::before,.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-0.5*var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-start>.popover-arrow,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow{right:calc(-1*(var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-start>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after{border-width:calc(var(--bs-popover-arrow-width)*.5) 0 calc(var(--bs-popover-arrow-width)*.5) var(--bs-popover-arrow-height)}.bs-popover-start>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-start>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y;-webkit-touch-action:pan-y;-moz-touch-action:pan-y;-ms-touch-action:pan-y;-o-touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;backface-visibility:hidden;-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;-ms-backface-visibility:hidden;-o-backface-visibility:hidden;transition:transform .6s ease-in-out}@media(prefers-reduced-motion: reduce){.carousel-item{transition:none}}.carousel-item.active,.carousel-item-next,.carousel-item-prev{display:block}.carousel-item-next:not(.carousel-item-start),.active.carousel-item-end{transform:translateX(100%)}.carousel-item-prev:not(.carousel-item-end),.active.carousel-item-start{transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item.active,.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end{z-index:1;opacity:1}.carousel-fade .active.carousel-item-start,.carousel-fade .active.carousel-item-end{z-index:0;opacity:0;transition:opacity 0s .6s}@media(prefers-reduced-motion: reduce){.carousel-fade .active.carousel-item-start,.carousel-fade .active.carousel-item-end{transition:none}}.carousel-control-prev,.carousel-control-next{position:absolute;top:0;bottom:0;z-index:1;display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;justify-content:center;-webkit-justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:none;border:0;opacity:.5;transition:opacity .15s ease}@media(prefers-reduced-motion: reduce){.carousel-control-prev,.carousel-control-next{transition:none}}.carousel-control-prev:hover,.carousel-control-prev:focus,.carousel-control-next:hover,.carousel-control-next:focus{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-prev-icon,.carousel-control-next-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;display:-webkit-flex;justify-content:center;-webkit-justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;-webkit-flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid rgba(0,0,0,0);border-bottom:10px solid rgba(0,0,0,0);opacity:.5;transition:opacity .6s ease}@media(prefers-reduced-motion: reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-prev-icon,.carousel-dark .carousel-control-next-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}[data-bs-theme=dark] .carousel .carousel-control-prev-icon,[data-bs-theme=dark] .carousel .carousel-control-next-icon,[data-bs-theme=dark].carousel .carousel-control-prev-icon,[data-bs-theme=dark].carousel .carousel-control-next-icon{filter:invert(1) grayscale(100)}[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target],[data-bs-theme=dark].carousel .carousel-indicators [data-bs-target]{background-color:#000}[data-bs-theme=dark] .carousel .carousel-caption,[data-bs-theme=dark].carousel .carousel-caption{color:#000}.spinner-grow,.spinner-border{display:inline-block;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg) /* rtl:ignore */}}.spinner-border{--bs-spinner-width: 2rem;--bs-spinner-height: 2rem;--bs-spinner-vertical-align: -0.125em;--bs-spinner-border-width: 0.25em;--bs-spinner-animation-speed: 0.75s;--bs-spinner-animation-name: spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:rgba(0,0,0,0)}.spinner-border-sm{--bs-spinner-width: 1rem;--bs-spinner-height: 1rem;--bs-spinner-border-width: 0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width: 2rem;--bs-spinner-height: 2rem;--bs-spinner-vertical-align: -0.125em;--bs-spinner-animation-speed: 0.75s;--bs-spinner-animation-name: spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width: 1rem;--bs-spinner-height: 1rem}@media(prefers-reduced-motion: reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed: 1.5s}}.offcanvas,.offcanvas-xxl,.offcanvas-xl,.offcanvas-lg,.offcanvas-md,.offcanvas-sm{--bs-offcanvas-zindex: 1045;--bs-offcanvas-width: 400px;--bs-offcanvas-height: 30vh;--bs-offcanvas-padding-x: 1rem;--bs-offcanvas-padding-y: 1rem;--bs-offcanvas-color: #000000;--bs-offcanvas-bg: #FFFFFF;--bs-offcanvas-border-width: 1px;--bs-offcanvas-border-color: rgba(0, 0, 0, 0.175);--bs-offcanvas-box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-offcanvas-transition: transform 0.3s ease-in-out;--bs-offcanvas-title-line-height: 1.5}@media(max-width: 575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media(max-width: 575.98px)and (prefers-reduced-motion: reduce){.offcanvas-sm{transition:none}}@media(max-width: 575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.showing,.offcanvas-sm.show:not(.hiding){transform:none}.offcanvas-sm.showing,.offcanvas-sm.hiding,.offcanvas-sm.show{visibility:visible}}@media(min-width: 576px){.offcanvas-sm{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:rgba(0,0,0,0) !important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible;background-color:rgba(0,0,0,0) !important}}@media(max-width: 767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media(max-width: 767.98px)and (prefers-reduced-motion: reduce){.offcanvas-md{transition:none}}@media(max-width: 767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.showing,.offcanvas-md.show:not(.hiding){transform:none}.offcanvas-md.showing,.offcanvas-md.hiding,.offcanvas-md.show{visibility:visible}}@media(min-width: 768px){.offcanvas-md{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:rgba(0,0,0,0) !important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible;background-color:rgba(0,0,0,0) !important}}@media(max-width: 991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media(max-width: 991.98px)and (prefers-reduced-motion: reduce){.offcanvas-lg{transition:none}}@media(max-width: 991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.showing,.offcanvas-lg.show:not(.hiding){transform:none}.offcanvas-lg.showing,.offcanvas-lg.hiding,.offcanvas-lg.show{visibility:visible}}@media(min-width: 992px){.offcanvas-lg{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:rgba(0,0,0,0) !important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible;background-color:rgba(0,0,0,0) !important}}@media(max-width: 1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media(max-width: 1199.98px)and (prefers-reduced-motion: reduce){.offcanvas-xl{transition:none}}@media(max-width: 1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.showing,.offcanvas-xl.show:not(.hiding){transform:none}.offcanvas-xl.showing,.offcanvas-xl.hiding,.offcanvas-xl.show{visibility:visible}}@media(min-width: 1200px){.offcanvas-xl{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:rgba(0,0,0,0) !important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible;background-color:rgba(0,0,0,0) !important}}@media(max-width: 1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media(max-width: 1399.98px)and (prefers-reduced-motion: reduce){.offcanvas-xxl{transition:none}}@media(max-width: 1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.showing,.offcanvas-xxl.show:not(.hiding){transform:none}.offcanvas-xxl.showing,.offcanvas-xxl.hiding,.offcanvas-xxl.show{visibility:visible}}@media(min-width: 1400px){.offcanvas-xxl{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:rgba(0,0,0,0) !important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible;background-color:rgba(0,0,0,0) !important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}@media(prefers-reduced-motion: reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.showing,.offcanvas.show:not(.hiding){transform:none}.offcanvas.showing,.offcanvas.hiding,.offcanvas.show{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;justify-content:space-between;-webkit-justify-content:space-between;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y)*.5) calc(var(--bs-offcanvas-padding-x)*.5);margin-top:calc(-0.5*var(--bs-offcanvas-padding-y));margin-right:calc(-0.5*var(--bs-offcanvas-padding-x));margin-bottom:calc(-0.5*var(--bs-offcanvas-padding-y))}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;-webkit-flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{mask-image:linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);-webkit-mask-image:linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);mask-size:200% 100%;-webkit-mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{mask-position:-200% 0%;-webkit-mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-default{color:#fff !important;background-color:RGBA(var(--bs-default-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-primary{color:#000 !important;background-color:RGBA(var(--bs-primary-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-secondary{color:#fff !important;background-color:RGBA(var(--bs-secondary-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-success{color:#fff !important;background-color:RGBA(var(--bs-success-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-info{color:#fff !important;background-color:RGBA(var(--bs-info-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-warning{color:#fff !important;background-color:RGBA(var(--bs-warning-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-danger{color:#fff !important;background-color:RGBA(var(--bs-danger-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-light{color:#000 !important;background-color:RGBA(var(--bs-light-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-dark{color:#fff !important;background-color:RGBA(var(--bs-dark-rgb), var(--bs-bg-opacity, 1)) !important}.link-default{color:RGBA(var(--bs-default-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-default-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-default:hover,.link-default:focus{color:RGBA(42, 46, 51, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(42, 46, 51, var(--bs-link-underline-opacity, 1)) !important}.link-primary{color:RGBA(var(--bs-primary-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-primary-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-primary:hover,.link-primary:focus{color:RGBA(237, 237, 253, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(237, 237, 253, var(--bs-link-underline-opacity, 1)) !important}.link-secondary{color:RGBA(var(--bs-secondary-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-secondary:hover,.link-secondary:focus{color:RGBA(42, 46, 51, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(42, 46, 51, var(--bs-link-underline-opacity, 1)) !important}.link-success{color:RGBA(var(--bs-success-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-success-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-success:hover,.link-success:focus{color:RGBA(50, 146, 19, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(50, 146, 19, var(--bs-link-underline-opacity, 1)) !important}.link-info{color:RGBA(var(--bs-info-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-info-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-info:hover,.link-info:focus{color:RGBA(122, 67, 150, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(122, 67, 150, var(--bs-link-underline-opacity, 1)) !important}.link-warning{color:RGBA(var(--bs-warning-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-warning-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-warning:hover,.link-warning:focus{color:RGBA(204, 94, 19, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(204, 94, 19, var(--bs-link-underline-opacity, 1)) !important}.link-danger{color:RGBA(var(--bs-danger-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-danger-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-danger:hover,.link-danger:focus{color:RGBA(204, 0, 46, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(204, 0, 46, var(--bs-link-underline-opacity, 1)) !important}.link-light{color:RGBA(var(--bs-light-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-light-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-light:hover,.link-light:focus{color:RGBA(249, 250, 251, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(249, 250, 251, var(--bs-link-underline-opacity, 1)) !important}.link-dark{color:RGBA(var(--bs-dark-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-dark-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-dark:hover,.link-dark:focus{color:RGBA(42, 46, 51, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(42, 46, 51, var(--bs-link-underline-opacity, 1)) !important}.link-body-emphasis{color:RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-body-emphasis:hover,.link-body-emphasis:focus{color:RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 0.75)) !important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 0.75)) !important}.focus-ring:focus{outline:0;box-shadow:var(--bs-focus-ring-x, 0) var(--bs-focus-ring-y, 0) var(--bs-focus-ring-blur, 0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}.icon-link{display:inline-flex;gap:.375rem;align-items:center;-webkit-align-items:center;text-decoration-color:rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 0.5));text-underline-offset:.25em;backface-visibility:hidden;-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;-ms-backface-visibility:hidden;-o-backface-visibility:hidden}.icon-link>.bi{flex-shrink:0;-webkit-flex-shrink:0;width:1em;height:1em;fill:currentcolor;transition:.2s ease-in-out transform}@media(prefers-reduced-motion: reduce){.icon-link>.bi{transition:none}}.icon-link-hover:hover>.bi,.icon-link-hover:focus-visible>.bi{transform:var(--bs-icon-link-transform, translate3d(0.25em, 0, 0))}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio: 100%}.ratio-4x3{--bs-aspect-ratio: 75%}.ratio-16x9{--bs-aspect-ratio: 56.25%}.ratio-21x9{--bs-aspect-ratio: 42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:sticky;top:0;z-index:1020}.sticky-bottom{position:sticky;bottom:0;z-index:1020}@media(min-width: 576px){.sticky-sm-top{position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:sticky;bottom:0;z-index:1020}}@media(min-width: 768px){.sticky-md-top{position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:sticky;bottom:0;z-index:1020}}@media(min-width: 992px){.sticky-lg-top{position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:sticky;bottom:0;z-index:1020}}@media(min-width: 1200px){.sticky-xl-top{position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:sticky;bottom:0;z-index:1020}}@media(min-width: 1400px){.sticky-xxl-top{position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;display:-webkit-flex;flex-direction:row;-webkit-flex-direction:row;align-items:center;-webkit-align-items:center;align-self:stretch;-webkit-align-self:stretch}.vstack{display:flex;display:-webkit-flex;flex:1 1 auto;-webkit-flex:1 1 auto;flex-direction:column;-webkit-flex-direction:column;align-self:stretch;-webkit-align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){width:1px !important;height:1px !important;padding:0 !important;margin:-1px !important;overflow:hidden !important;clip:rect(0, 0, 0, 0) !important;white-space:nowrap !important;border:0 !important}.visually-hidden:not(caption),.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption){position:absolute !important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;-webkit-align-self:stretch;width:1px;min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline !important}.align-top{vertical-align:top !important}.align-middle{vertical-align:middle !important}.align-bottom{vertical-align:bottom !important}.align-text-bottom{vertical-align:text-bottom !important}.align-text-top{vertical-align:text-top !important}.float-start{float:left !important}.float-end{float:right !important}.float-none{float:none !important}.object-fit-contain{object-fit:contain !important}.object-fit-cover{object-fit:cover !important}.object-fit-fill{object-fit:fill !important}.object-fit-scale{object-fit:scale-down !important}.object-fit-none{object-fit:none !important}.opacity-0{opacity:0 !important}.opacity-25{opacity:.25 !important}.opacity-50{opacity:.5 !important}.opacity-75{opacity:.75 !important}.opacity-100{opacity:1 !important}.overflow-auto{overflow:auto !important}.overflow-hidden{overflow:hidden !important}.overflow-visible{overflow:visible !important}.overflow-scroll{overflow:scroll !important}.overflow-x-auto{overflow-x:auto !important}.overflow-x-hidden{overflow-x:hidden !important}.overflow-x-visible{overflow-x:visible !important}.overflow-x-scroll{overflow-x:scroll !important}.overflow-y-auto{overflow-y:auto !important}.overflow-y-hidden{overflow-y:hidden !important}.overflow-y-visible{overflow-y:visible !important}.overflow-y-scroll{overflow-y:scroll !important}.d-inline{display:inline !important}.d-inline-block{display:inline-block !important}.d-block{display:block !important}.d-grid{display:grid !important}.d-inline-grid{display:inline-grid !important}.d-table{display:table !important}.d-table-row{display:table-row !important}.d-table-cell{display:table-cell !important}.d-flex{display:flex !important}.d-inline-flex{display:inline-flex !important}.d-none{display:none !important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15) !important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075) !important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175) !important}.shadow-none{box-shadow:none !important}.focus-ring-default{--bs-focus-ring-color: rgba(var(--bs-default-rgb), var(--bs-focus-ring-opacity))}.focus-ring-primary{--bs-focus-ring-color: rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-secondary{--bs-focus-ring-color: rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-success{--bs-focus-ring-color: rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity))}.focus-ring-info{--bs-focus-ring-color: rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity))}.focus-ring-warning{--bs-focus-ring-color: rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity))}.focus-ring-danger{--bs-focus-ring-color: rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity))}.focus-ring-light{--bs-focus-ring-color: rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity))}.focus-ring-dark{--bs-focus-ring-color: rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity))}.position-static{position:static !important}.position-relative{position:relative !important}.position-absolute{position:absolute !important}.position-fixed{position:fixed !important}.position-sticky{position:sticky !important}.top-0{top:0 !important}.top-50{top:50% !important}.top-100{top:100% !important}.bottom-0{bottom:0 !important}.bottom-50{bottom:50% !important}.bottom-100{bottom:100% !important}.start-0{left:0 !important}.start-50{left:50% !important}.start-100{left:100% !important}.end-0{right:0 !important}.end-50{right:50% !important}.end-100{right:100% !important}.translate-middle{transform:translate(-50%, -50%) !important}.translate-middle-x{transform:translateX(-50%) !important}.translate-middle-y{transform:translateY(-50%) !important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important}.border-0{border:0 !important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important}.border-top-0{border-top:0 !important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important}.border-end-0{border-right:0 !important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important}.border-bottom-0{border-bottom:0 !important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important}.border-start-0{border-left:0 !important}.border-default{--bs-border-opacity: 1;border-color:rgba(var(--bs-default-rgb), var(--bs-border-opacity)) !important}.border-primary{--bs-border-opacity: 1;border-color:rgba(var(--bs-primary-rgb), var(--bs-border-opacity)) !important}.border-secondary{--bs-border-opacity: 1;border-color:rgba(var(--bs-secondary-rgb), var(--bs-border-opacity)) !important}.border-success{--bs-border-opacity: 1;border-color:rgba(var(--bs-success-rgb), var(--bs-border-opacity)) !important}.border-info{--bs-border-opacity: 1;border-color:rgba(var(--bs-info-rgb), var(--bs-border-opacity)) !important}.border-warning{--bs-border-opacity: 1;border-color:rgba(var(--bs-warning-rgb), var(--bs-border-opacity)) !important}.border-danger{--bs-border-opacity: 1;border-color:rgba(var(--bs-danger-rgb), var(--bs-border-opacity)) !important}.border-light{--bs-border-opacity: 1;border-color:rgba(var(--bs-light-rgb), var(--bs-border-opacity)) !important}.border-dark{--bs-border-opacity: 1;border-color:rgba(var(--bs-dark-rgb), var(--bs-border-opacity)) !important}.border-black{--bs-border-opacity: 1;border-color:rgba(var(--bs-black-rgb), var(--bs-border-opacity)) !important}.border-white{--bs-border-opacity: 1;border-color:rgba(var(--bs-white-rgb), var(--bs-border-opacity)) !important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle) !important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle) !important}.border-success-subtle{border-color:var(--bs-success-border-subtle) !important}.border-info-subtle{border-color:var(--bs-info-border-subtle) !important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle) !important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle) !important}.border-light-subtle{border-color:var(--bs-light-border-subtle) !important}.border-dark-subtle{border-color:var(--bs-dark-border-subtle) !important}.border-1{border-width:1px !important}.border-2{border-width:2px !important}.border-3{border-width:3px !important}.border-4{border-width:4px !important}.border-5{border-width:5px !important}.border-opacity-10{--bs-border-opacity: 0.1}.border-opacity-25{--bs-border-opacity: 0.25}.border-opacity-50{--bs-border-opacity: 0.5}.border-opacity-75{--bs-border-opacity: 0.75}.border-opacity-100{--bs-border-opacity: 1}.w-25{width:25% !important}.w-50{width:50% !important}.w-75{width:75% !important}.w-100{width:100% !important}.w-auto{width:auto !important}.mw-100{max-width:100% !important}.vw-100{width:100vw !important}.min-vw-100{min-width:100vw !important}.h-25{height:25% !important}.h-50{height:50% !important}.h-75{height:75% !important}.h-100{height:100% !important}.h-auto{height:auto !important}.mh-100{max-height:100% !important}.vh-100{height:100vh !important}.min-vh-100{min-height:100vh !important}.flex-fill{flex:1 1 auto !important}.flex-row{flex-direction:row !important}.flex-column{flex-direction:column !important}.flex-row-reverse{flex-direction:row-reverse !important}.flex-column-reverse{flex-direction:column-reverse !important}.flex-grow-0{flex-grow:0 !important}.flex-grow-1{flex-grow:1 !important}.flex-shrink-0{flex-shrink:0 !important}.flex-shrink-1{flex-shrink:1 !important}.flex-wrap{flex-wrap:wrap !important}.flex-nowrap{flex-wrap:nowrap !important}.flex-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-start{justify-content:flex-start !important}.justify-content-end{justify-content:flex-end !important}.justify-content-center{justify-content:center !important}.justify-content-between{justify-content:space-between !important}.justify-content-around{justify-content:space-around !important}.justify-content-evenly{justify-content:space-evenly !important}.align-items-start{align-items:flex-start !important}.align-items-end{align-items:flex-end !important}.align-items-center{align-items:center !important}.align-items-baseline{align-items:baseline !important}.align-items-stretch{align-items:stretch !important}.align-content-start{align-content:flex-start !important}.align-content-end{align-content:flex-end !important}.align-content-center{align-content:center !important}.align-content-between{align-content:space-between !important}.align-content-around{align-content:space-around !important}.align-content-stretch{align-content:stretch !important}.align-self-auto{align-self:auto !important}.align-self-start{align-self:flex-start !important}.align-self-end{align-self:flex-end !important}.align-self-center{align-self:center !important}.align-self-baseline{align-self:baseline !important}.align-self-stretch{align-self:stretch !important}.order-first{order:-1 !important}.order-0{order:0 !important}.order-1{order:1 !important}.order-2{order:2 !important}.order-3{order:3 !important}.order-4{order:4 !important}.order-5{order:5 !important}.order-last{order:6 !important}.m-0{margin:0 !important}.m-1{margin:.25rem !important}.m-2{margin:.5rem !important}.m-3{margin:1rem !important}.m-4{margin:1.5rem !important}.m-5{margin:3rem !important}.m-auto{margin:auto !important}.mx-0{margin-right:0 !important;margin-left:0 !important}.mx-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-3{margin-right:1rem !important;margin-left:1rem !important}.mx-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-5{margin-right:3rem !important;margin-left:3rem !important}.mx-auto{margin-right:auto !important;margin-left:auto !important}.my-0{margin-top:0 !important;margin-bottom:0 !important}.my-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-0{margin-top:0 !important}.mt-1{margin-top:.25rem !important}.mt-2{margin-top:.5rem !important}.mt-3{margin-top:1rem !important}.mt-4{margin-top:1.5rem !important}.mt-5{margin-top:3rem !important}.mt-auto{margin-top:auto !important}.me-0{margin-right:0 !important}.me-1{margin-right:.25rem !important}.me-2{margin-right:.5rem !important}.me-3{margin-right:1rem !important}.me-4{margin-right:1.5rem !important}.me-5{margin-right:3rem !important}.me-auto{margin-right:auto !important}.mb-0{margin-bottom:0 !important}.mb-1{margin-bottom:.25rem !important}.mb-2{margin-bottom:.5rem !important}.mb-3{margin-bottom:1rem !important}.mb-4{margin-bottom:1.5rem !important}.mb-5{margin-bottom:3rem !important}.mb-auto{margin-bottom:auto !important}.ms-0{margin-left:0 !important}.ms-1{margin-left:.25rem !important}.ms-2{margin-left:.5rem !important}.ms-3{margin-left:1rem !important}.ms-4{margin-left:1.5rem !important}.ms-5{margin-left:3rem !important}.ms-auto{margin-left:auto !important}.p-0{padding:0 !important}.p-1{padding:.25rem !important}.p-2{padding:.5rem !important}.p-3{padding:1rem !important}.p-4{padding:1.5rem !important}.p-5{padding:3rem !important}.px-0{padding-right:0 !important;padding-left:0 !important}.px-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-3{padding-right:1rem !important;padding-left:1rem !important}.px-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-5{padding-right:3rem !important;padding-left:3rem !important}.py-0{padding-top:0 !important;padding-bottom:0 !important}.py-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-0{padding-top:0 !important}.pt-1{padding-top:.25rem !important}.pt-2{padding-top:.5rem !important}.pt-3{padding-top:1rem !important}.pt-4{padding-top:1.5rem !important}.pt-5{padding-top:3rem !important}.pe-0{padding-right:0 !important}.pe-1{padding-right:.25rem !important}.pe-2{padding-right:.5rem !important}.pe-3{padding-right:1rem !important}.pe-4{padding-right:1.5rem !important}.pe-5{padding-right:3rem !important}.pb-0{padding-bottom:0 !important}.pb-1{padding-bottom:.25rem !important}.pb-2{padding-bottom:.5rem !important}.pb-3{padding-bottom:1rem !important}.pb-4{padding-bottom:1.5rem !important}.pb-5{padding-bottom:3rem !important}.ps-0{padding-left:0 !important}.ps-1{padding-left:.25rem !important}.ps-2{padding-left:.5rem !important}.ps-3{padding-left:1rem !important}.ps-4{padding-left:1.5rem !important}.ps-5{padding-left:3rem !important}.gap-0{gap:0 !important}.gap-1{gap:.25rem !important}.gap-2{gap:.5rem !important}.gap-3{gap:1rem !important}.gap-4{gap:1.5rem !important}.gap-5{gap:3rem !important}.row-gap-0{row-gap:0 !important}.row-gap-1{row-gap:.25rem !important}.row-gap-2{row-gap:.5rem !important}.row-gap-3{row-gap:1rem !important}.row-gap-4{row-gap:1.5rem !important}.row-gap-5{row-gap:3rem !important}.column-gap-0{column-gap:0 !important}.column-gap-1{column-gap:.25rem !important}.column-gap-2{column-gap:.5rem !important}.column-gap-3{column-gap:1rem !important}.column-gap-4{column-gap:1.5rem !important}.column-gap-5{column-gap:3rem !important}.font-monospace{font-family:var(--bs-font-monospace) !important}.fs-1{font-size:calc(1.325rem + 0.9vw) !important}.fs-2{font-size:calc(1.29rem + 0.48vw) !important}.fs-3{font-size:calc(1.27rem + 0.24vw) !important}.fs-4{font-size:1.25rem !important}.fs-5{font-size:1.1rem !important}.fs-6{font-size:1rem !important}.fst-italic{font-style:italic !important}.fst-normal{font-style:normal !important}.fw-lighter{font-weight:lighter !important}.fw-light{font-weight:300 !important}.fw-normal{font-weight:400 !important}.fw-medium{font-weight:500 !important}.fw-semibold{font-weight:600 !important}.fw-bold{font-weight:700 !important}.fw-bolder{font-weight:bolder !important}.lh-1{line-height:1 !important}.lh-sm{line-height:1.25 !important}.lh-base{line-height:1.5 !important}.lh-lg{line-height:2 !important}.text-start{text-align:left !important}.text-end{text-align:right !important}.text-center{text-align:center !important}.text-decoration-none{text-decoration:none !important}.text-decoration-underline{text-decoration:underline !important}.text-decoration-line-through{text-decoration:line-through !important}.text-lowercase{text-transform:lowercase !important}.text-uppercase{text-transform:uppercase !important}.text-capitalize{text-transform:capitalize !important}.text-wrap{white-space:normal !important}.text-nowrap{white-space:nowrap !important}.text-break{word-wrap:break-word !important;word-break:break-word !important}.text-default{--bs-text-opacity: 1;color:rgba(var(--bs-default-rgb), var(--bs-text-opacity)) !important}.text-primary{--bs-text-opacity: 1;color:rgba(var(--bs-primary-rgb), var(--bs-text-opacity)) !important}.text-secondary{--bs-text-opacity: 1;color:rgba(var(--bs-secondary-rgb), var(--bs-text-opacity)) !important}.text-success{--bs-text-opacity: 1;color:rgba(var(--bs-success-rgb), var(--bs-text-opacity)) !important}.text-info{--bs-text-opacity: 1;color:rgba(var(--bs-info-rgb), var(--bs-text-opacity)) !important}.text-warning{--bs-text-opacity: 1;color:rgba(var(--bs-warning-rgb), var(--bs-text-opacity)) !important}.text-danger{--bs-text-opacity: 1;color:rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important}.text-light{--bs-text-opacity: 1;color:rgba(var(--bs-light-rgb), var(--bs-text-opacity)) !important}.text-dark{--bs-text-opacity: 1;color:rgba(var(--bs-dark-rgb), var(--bs-text-opacity)) !important}.text-black{--bs-text-opacity: 1;color:rgba(var(--bs-black-rgb), var(--bs-text-opacity)) !important}.text-white{--bs-text-opacity: 1;color:rgba(var(--bs-white-rgb), var(--bs-text-opacity)) !important}.text-body{--bs-text-opacity: 1;color:rgba(var(--bs-body-color-rgb), var(--bs-text-opacity)) !important}.text-muted{--bs-text-opacity: 1;color:var(--bs-secondary-color) !important}.text-black-50{--bs-text-opacity: 1;color:rgba(0,0,0,.5) !important}.text-white-50{--bs-text-opacity: 1;color:rgba(255,255,255,.5) !important}.text-body-secondary{--bs-text-opacity: 1;color:var(--bs-secondary-color) !important}.text-body-tertiary{--bs-text-opacity: 1;color:var(--bs-tertiary-color) !important}.text-body-emphasis{--bs-text-opacity: 1;color:var(--bs-emphasis-color) !important}.text-reset{--bs-text-opacity: 1;color:inherit !important}.text-opacity-25{--bs-text-opacity: 0.25}.text-opacity-50{--bs-text-opacity: 0.5}.text-opacity-75{--bs-text-opacity: 0.75}.text-opacity-100{--bs-text-opacity: 1}.text-primary-emphasis{color:var(--bs-primary-text-emphasis) !important}.text-secondary-emphasis{color:var(--bs-secondary-text-emphasis) !important}.text-success-emphasis{color:var(--bs-success-text-emphasis) !important}.text-info-emphasis{color:var(--bs-info-text-emphasis) !important}.text-warning-emphasis{color:var(--bs-warning-text-emphasis) !important}.text-danger-emphasis{color:var(--bs-danger-text-emphasis) !important}.text-light-emphasis{color:var(--bs-light-text-emphasis) !important}.text-dark-emphasis{color:var(--bs-dark-text-emphasis) !important}.link-opacity-10{--bs-link-opacity: 0.1}.link-opacity-10-hover:hover{--bs-link-opacity: 0.1}.link-opacity-25{--bs-link-opacity: 0.25}.link-opacity-25-hover:hover{--bs-link-opacity: 0.25}.link-opacity-50{--bs-link-opacity: 0.5}.link-opacity-50-hover:hover{--bs-link-opacity: 0.5}.link-opacity-75{--bs-link-opacity: 0.75}.link-opacity-75-hover:hover{--bs-link-opacity: 0.75}.link-opacity-100{--bs-link-opacity: 1}.link-opacity-100-hover:hover{--bs-link-opacity: 1}.link-offset-1{text-underline-offset:.125em !important}.link-offset-1-hover:hover{text-underline-offset:.125em !important}.link-offset-2{text-underline-offset:.25em !important}.link-offset-2-hover:hover{text-underline-offset:.25em !important}.link-offset-3{text-underline-offset:.375em !important}.link-offset-3-hover:hover{text-underline-offset:.375em !important}.link-underline-default{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-default-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-primary{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-primary-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-secondary{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-secondary-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-success{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-success-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-info{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-info-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-warning{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-warning-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-danger{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-danger-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-light{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-light-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-dark{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-dark-rgb), var(--bs-link-underline-opacity)) !important}.link-underline{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-link-color-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-underline-opacity-0{--bs-link-underline-opacity: 0}.link-underline-opacity-0-hover:hover{--bs-link-underline-opacity: 0}.link-underline-opacity-10{--bs-link-underline-opacity: 0.1}.link-underline-opacity-10-hover:hover{--bs-link-underline-opacity: 0.1}.link-underline-opacity-25{--bs-link-underline-opacity: 0.25}.link-underline-opacity-25-hover:hover{--bs-link-underline-opacity: 0.25}.link-underline-opacity-50{--bs-link-underline-opacity: 0.5}.link-underline-opacity-50-hover:hover{--bs-link-underline-opacity: 0.5}.link-underline-opacity-75{--bs-link-underline-opacity: 0.75}.link-underline-opacity-75-hover:hover{--bs-link-underline-opacity: 0.75}.link-underline-opacity-100{--bs-link-underline-opacity: 1}.link-underline-opacity-100-hover:hover{--bs-link-underline-opacity: 1}.bg-default{--bs-bg-opacity: 1;background-color:rgba(var(--bs-default-rgb), var(--bs-bg-opacity)) !important}.bg-primary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-primary-rgb), var(--bs-bg-opacity)) !important}.bg-secondary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-secondary-rgb), var(--bs-bg-opacity)) !important}.bg-success{--bs-bg-opacity: 1;background-color:rgba(var(--bs-success-rgb), var(--bs-bg-opacity)) !important}.bg-info{--bs-bg-opacity: 1;background-color:rgba(var(--bs-info-rgb), var(--bs-bg-opacity)) !important}.bg-warning{--bs-bg-opacity: 1;background-color:rgba(var(--bs-warning-rgb), var(--bs-bg-opacity)) !important}.bg-danger{--bs-bg-opacity: 1;background-color:rgba(var(--bs-danger-rgb), var(--bs-bg-opacity)) !important}.bg-light{--bs-bg-opacity: 1;background-color:rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important}.bg-dark{--bs-bg-opacity: 1;background-color:rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important}.bg-black{--bs-bg-opacity: 1;background-color:rgba(var(--bs-black-rgb), var(--bs-bg-opacity)) !important}.bg-white{--bs-bg-opacity: 1;background-color:rgba(var(--bs-white-rgb), var(--bs-bg-opacity)) !important}.bg-body{--bs-bg-opacity: 1;background-color:rgba(var(--bs-body-bg-rgb), var(--bs-bg-opacity)) !important}.bg-transparent{--bs-bg-opacity: 1;background-color:rgba(0,0,0,0) !important}.bg-body-secondary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-secondary-bg-rgb), var(--bs-bg-opacity)) !important}.bg-body-tertiary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-tertiary-bg-rgb), var(--bs-bg-opacity)) !important}.bg-opacity-10{--bs-bg-opacity: 0.1}.bg-opacity-25{--bs-bg-opacity: 0.25}.bg-opacity-50{--bs-bg-opacity: 0.5}.bg-opacity-75{--bs-bg-opacity: 0.75}.bg-opacity-100{--bs-bg-opacity: 1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle) !important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle) !important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle) !important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle) !important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle) !important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle) !important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle) !important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle) !important}.bg-gradient{background-image:var(--bs-gradient) !important}.user-select-all{user-select:all !important}.user-select-auto{user-select:auto !important}.user-select-none{user-select:none !important}.pe-none{pointer-events:none !important}.pe-auto{pointer-events:auto !important}.rounded{border-radius:var(--bs-border-radius) !important}.rounded-0{border-radius:0 !important}.rounded-1{border-radius:var(--bs-border-radius-sm) !important}.rounded-2{border-radius:var(--bs-border-radius) !important}.rounded-3{border-radius:var(--bs-border-radius-lg) !important}.rounded-4{border-radius:var(--bs-border-radius-xl) !important}.rounded-5{border-radius:var(--bs-border-radius-xxl) !important}.rounded-circle{border-radius:50% !important}.rounded-pill{border-radius:var(--bs-border-radius-pill) !important}.rounded-top{border-top-left-radius:var(--bs-border-radius) !important;border-top-right-radius:var(--bs-border-radius) !important}.rounded-top-0{border-top-left-radius:0 !important;border-top-right-radius:0 !important}.rounded-top-1{border-top-left-radius:var(--bs-border-radius-sm) !important;border-top-right-radius:var(--bs-border-radius-sm) !important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius) !important;border-top-right-radius:var(--bs-border-radius) !important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg) !important;border-top-right-radius:var(--bs-border-radius-lg) !important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl) !important;border-top-right-radius:var(--bs-border-radius-xl) !important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-xxl) !important;border-top-right-radius:var(--bs-border-radius-xxl) !important}.rounded-top-circle{border-top-left-radius:50% !important;border-top-right-radius:50% !important}.rounded-top-pill{border-top-left-radius:var(--bs-border-radius-pill) !important;border-top-right-radius:var(--bs-border-radius-pill) !important}.rounded-end{border-top-right-radius:var(--bs-border-radius) !important;border-bottom-right-radius:var(--bs-border-radius) !important}.rounded-end-0{border-top-right-radius:0 !important;border-bottom-right-radius:0 !important}.rounded-end-1{border-top-right-radius:var(--bs-border-radius-sm) !important;border-bottom-right-radius:var(--bs-border-radius-sm) !important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius) !important;border-bottom-right-radius:var(--bs-border-radius) !important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg) !important;border-bottom-right-radius:var(--bs-border-radius-lg) !important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl) !important;border-bottom-right-radius:var(--bs-border-radius-xl) !important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-xxl) !important;border-bottom-right-radius:var(--bs-border-radius-xxl) !important}.rounded-end-circle{border-top-right-radius:50% !important;border-bottom-right-radius:50% !important}.rounded-end-pill{border-top-right-radius:var(--bs-border-radius-pill) !important;border-bottom-right-radius:var(--bs-border-radius-pill) !important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius) !important;border-bottom-left-radius:var(--bs-border-radius) !important}.rounded-bottom-0{border-bottom-right-radius:0 !important;border-bottom-left-radius:0 !important}.rounded-bottom-1{border-bottom-right-radius:var(--bs-border-radius-sm) !important;border-bottom-left-radius:var(--bs-border-radius-sm) !important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius) !important;border-bottom-left-radius:var(--bs-border-radius) !important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg) !important;border-bottom-left-radius:var(--bs-border-radius-lg) !important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl) !important;border-bottom-left-radius:var(--bs-border-radius-xl) !important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-xxl) !important;border-bottom-left-radius:var(--bs-border-radius-xxl) !important}.rounded-bottom-circle{border-bottom-right-radius:50% !important;border-bottom-left-radius:50% !important}.rounded-bottom-pill{border-bottom-right-radius:var(--bs-border-radius-pill) !important;border-bottom-left-radius:var(--bs-border-radius-pill) !important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius) !important;border-top-left-radius:var(--bs-border-radius) !important}.rounded-start-0{border-bottom-left-radius:0 !important;border-top-left-radius:0 !important}.rounded-start-1{border-bottom-left-radius:var(--bs-border-radius-sm) !important;border-top-left-radius:var(--bs-border-radius-sm) !important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius) !important;border-top-left-radius:var(--bs-border-radius) !important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg) !important;border-top-left-radius:var(--bs-border-radius-lg) !important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl) !important;border-top-left-radius:var(--bs-border-radius-xl) !important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-xxl) !important;border-top-left-radius:var(--bs-border-radius-xxl) !important}.rounded-start-circle{border-bottom-left-radius:50% !important;border-top-left-radius:50% !important}.rounded-start-pill{border-bottom-left-radius:var(--bs-border-radius-pill) !important;border-top-left-radius:var(--bs-border-radius-pill) !important}.visible{visibility:visible !important}.invisible{visibility:hidden !important}.z-n1{z-index:-1 !important}.z-0{z-index:0 !important}.z-1{z-index:1 !important}.z-2{z-index:2 !important}.z-3{z-index:3 !important}@media(min-width: 576px){.float-sm-start{float:left !important}.float-sm-end{float:right !important}.float-sm-none{float:none !important}.object-fit-sm-contain{object-fit:contain !important}.object-fit-sm-cover{object-fit:cover !important}.object-fit-sm-fill{object-fit:fill !important}.object-fit-sm-scale{object-fit:scale-down !important}.object-fit-sm-none{object-fit:none !important}.d-sm-inline{display:inline !important}.d-sm-inline-block{display:inline-block !important}.d-sm-block{display:block !important}.d-sm-grid{display:grid !important}.d-sm-inline-grid{display:inline-grid !important}.d-sm-table{display:table !important}.d-sm-table-row{display:table-row !important}.d-sm-table-cell{display:table-cell !important}.d-sm-flex{display:flex !important}.d-sm-inline-flex{display:inline-flex !important}.d-sm-none{display:none !important}.flex-sm-fill{flex:1 1 auto !important}.flex-sm-row{flex-direction:row !important}.flex-sm-column{flex-direction:column !important}.flex-sm-row-reverse{flex-direction:row-reverse !important}.flex-sm-column-reverse{flex-direction:column-reverse !important}.flex-sm-grow-0{flex-grow:0 !important}.flex-sm-grow-1{flex-grow:1 !important}.flex-sm-shrink-0{flex-shrink:0 !important}.flex-sm-shrink-1{flex-shrink:1 !important}.flex-sm-wrap{flex-wrap:wrap !important}.flex-sm-nowrap{flex-wrap:nowrap !important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-sm-start{justify-content:flex-start !important}.justify-content-sm-end{justify-content:flex-end !important}.justify-content-sm-center{justify-content:center !important}.justify-content-sm-between{justify-content:space-between !important}.justify-content-sm-around{justify-content:space-around !important}.justify-content-sm-evenly{justify-content:space-evenly !important}.align-items-sm-start{align-items:flex-start !important}.align-items-sm-end{align-items:flex-end !important}.align-items-sm-center{align-items:center !important}.align-items-sm-baseline{align-items:baseline !important}.align-items-sm-stretch{align-items:stretch !important}.align-content-sm-start{align-content:flex-start !important}.align-content-sm-end{align-content:flex-end !important}.align-content-sm-center{align-content:center !important}.align-content-sm-between{align-content:space-between !important}.align-content-sm-around{align-content:space-around !important}.align-content-sm-stretch{align-content:stretch !important}.align-self-sm-auto{align-self:auto !important}.align-self-sm-start{align-self:flex-start !important}.align-self-sm-end{align-self:flex-end !important}.align-self-sm-center{align-self:center !important}.align-self-sm-baseline{align-self:baseline !important}.align-self-sm-stretch{align-self:stretch !important}.order-sm-first{order:-1 !important}.order-sm-0{order:0 !important}.order-sm-1{order:1 !important}.order-sm-2{order:2 !important}.order-sm-3{order:3 !important}.order-sm-4{order:4 !important}.order-sm-5{order:5 !important}.order-sm-last{order:6 !important}.m-sm-0{margin:0 !important}.m-sm-1{margin:.25rem !important}.m-sm-2{margin:.5rem !important}.m-sm-3{margin:1rem !important}.m-sm-4{margin:1.5rem !important}.m-sm-5{margin:3rem !important}.m-sm-auto{margin:auto !important}.mx-sm-0{margin-right:0 !important;margin-left:0 !important}.mx-sm-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-sm-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-sm-3{margin-right:1rem !important;margin-left:1rem !important}.mx-sm-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-sm-5{margin-right:3rem !important;margin-left:3rem !important}.mx-sm-auto{margin-right:auto !important;margin-left:auto !important}.my-sm-0{margin-top:0 !important;margin-bottom:0 !important}.my-sm-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-sm-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-sm-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-sm-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-sm-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-sm-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-sm-0{margin-top:0 !important}.mt-sm-1{margin-top:.25rem !important}.mt-sm-2{margin-top:.5rem !important}.mt-sm-3{margin-top:1rem !important}.mt-sm-4{margin-top:1.5rem !important}.mt-sm-5{margin-top:3rem !important}.mt-sm-auto{margin-top:auto !important}.me-sm-0{margin-right:0 !important}.me-sm-1{margin-right:.25rem !important}.me-sm-2{margin-right:.5rem !important}.me-sm-3{margin-right:1rem !important}.me-sm-4{margin-right:1.5rem !important}.me-sm-5{margin-right:3rem !important}.me-sm-auto{margin-right:auto !important}.mb-sm-0{margin-bottom:0 !important}.mb-sm-1{margin-bottom:.25rem !important}.mb-sm-2{margin-bottom:.5rem !important}.mb-sm-3{margin-bottom:1rem !important}.mb-sm-4{margin-bottom:1.5rem !important}.mb-sm-5{margin-bottom:3rem !important}.mb-sm-auto{margin-bottom:auto !important}.ms-sm-0{margin-left:0 !important}.ms-sm-1{margin-left:.25rem !important}.ms-sm-2{margin-left:.5rem !important}.ms-sm-3{margin-left:1rem !important}.ms-sm-4{margin-left:1.5rem !important}.ms-sm-5{margin-left:3rem !important}.ms-sm-auto{margin-left:auto !important}.p-sm-0{padding:0 !important}.p-sm-1{padding:.25rem !important}.p-sm-2{padding:.5rem !important}.p-sm-3{padding:1rem !important}.p-sm-4{padding:1.5rem !important}.p-sm-5{padding:3rem !important}.px-sm-0{padding-right:0 !important;padding-left:0 !important}.px-sm-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-sm-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-sm-3{padding-right:1rem !important;padding-left:1rem !important}.px-sm-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-sm-5{padding-right:3rem !important;padding-left:3rem !important}.py-sm-0{padding-top:0 !important;padding-bottom:0 !important}.py-sm-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-sm-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-sm-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-sm-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-sm-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-sm-0{padding-top:0 !important}.pt-sm-1{padding-top:.25rem !important}.pt-sm-2{padding-top:.5rem !important}.pt-sm-3{padding-top:1rem !important}.pt-sm-4{padding-top:1.5rem !important}.pt-sm-5{padding-top:3rem !important}.pe-sm-0{padding-right:0 !important}.pe-sm-1{padding-right:.25rem !important}.pe-sm-2{padding-right:.5rem !important}.pe-sm-3{padding-right:1rem !important}.pe-sm-4{padding-right:1.5rem !important}.pe-sm-5{padding-right:3rem !important}.pb-sm-0{padding-bottom:0 !important}.pb-sm-1{padding-bottom:.25rem !important}.pb-sm-2{padding-bottom:.5rem !important}.pb-sm-3{padding-bottom:1rem !important}.pb-sm-4{padding-bottom:1.5rem !important}.pb-sm-5{padding-bottom:3rem !important}.ps-sm-0{padding-left:0 !important}.ps-sm-1{padding-left:.25rem !important}.ps-sm-2{padding-left:.5rem !important}.ps-sm-3{padding-left:1rem !important}.ps-sm-4{padding-left:1.5rem !important}.ps-sm-5{padding-left:3rem !important}.gap-sm-0{gap:0 !important}.gap-sm-1{gap:.25rem !important}.gap-sm-2{gap:.5rem !important}.gap-sm-3{gap:1rem !important}.gap-sm-4{gap:1.5rem !important}.gap-sm-5{gap:3rem !important}.row-gap-sm-0{row-gap:0 !important}.row-gap-sm-1{row-gap:.25rem !important}.row-gap-sm-2{row-gap:.5rem !important}.row-gap-sm-3{row-gap:1rem !important}.row-gap-sm-4{row-gap:1.5rem !important}.row-gap-sm-5{row-gap:3rem !important}.column-gap-sm-0{column-gap:0 !important}.column-gap-sm-1{column-gap:.25rem !important}.column-gap-sm-2{column-gap:.5rem !important}.column-gap-sm-3{column-gap:1rem !important}.column-gap-sm-4{column-gap:1.5rem !important}.column-gap-sm-5{column-gap:3rem !important}.text-sm-start{text-align:left !important}.text-sm-end{text-align:right !important}.text-sm-center{text-align:center !important}}@media(min-width: 768px){.float-md-start{float:left !important}.float-md-end{float:right !important}.float-md-none{float:none !important}.object-fit-md-contain{object-fit:contain !important}.object-fit-md-cover{object-fit:cover !important}.object-fit-md-fill{object-fit:fill !important}.object-fit-md-scale{object-fit:scale-down !important}.object-fit-md-none{object-fit:none !important}.d-md-inline{display:inline !important}.d-md-inline-block{display:inline-block !important}.d-md-block{display:block !important}.d-md-grid{display:grid !important}.d-md-inline-grid{display:inline-grid !important}.d-md-table{display:table !important}.d-md-table-row{display:table-row !important}.d-md-table-cell{display:table-cell !important}.d-md-flex{display:flex !important}.d-md-inline-flex{display:inline-flex !important}.d-md-none{display:none !important}.flex-md-fill{flex:1 1 auto !important}.flex-md-row{flex-direction:row !important}.flex-md-column{flex-direction:column !important}.flex-md-row-reverse{flex-direction:row-reverse !important}.flex-md-column-reverse{flex-direction:column-reverse !important}.flex-md-grow-0{flex-grow:0 !important}.flex-md-grow-1{flex-grow:1 !important}.flex-md-shrink-0{flex-shrink:0 !important}.flex-md-shrink-1{flex-shrink:1 !important}.flex-md-wrap{flex-wrap:wrap !important}.flex-md-nowrap{flex-wrap:nowrap !important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-md-start{justify-content:flex-start !important}.justify-content-md-end{justify-content:flex-end !important}.justify-content-md-center{justify-content:center !important}.justify-content-md-between{justify-content:space-between !important}.justify-content-md-around{justify-content:space-around !important}.justify-content-md-evenly{justify-content:space-evenly !important}.align-items-md-start{align-items:flex-start !important}.align-items-md-end{align-items:flex-end !important}.align-items-md-center{align-items:center !important}.align-items-md-baseline{align-items:baseline !important}.align-items-md-stretch{align-items:stretch !important}.align-content-md-start{align-content:flex-start !important}.align-content-md-end{align-content:flex-end !important}.align-content-md-center{align-content:center !important}.align-content-md-between{align-content:space-between !important}.align-content-md-around{align-content:space-around !important}.align-content-md-stretch{align-content:stretch !important}.align-self-md-auto{align-self:auto !important}.align-self-md-start{align-self:flex-start !important}.align-self-md-end{align-self:flex-end !important}.align-self-md-center{align-self:center !important}.align-self-md-baseline{align-self:baseline !important}.align-self-md-stretch{align-self:stretch !important}.order-md-first{order:-1 !important}.order-md-0{order:0 !important}.order-md-1{order:1 !important}.order-md-2{order:2 !important}.order-md-3{order:3 !important}.order-md-4{order:4 !important}.order-md-5{order:5 !important}.order-md-last{order:6 !important}.m-md-0{margin:0 !important}.m-md-1{margin:.25rem !important}.m-md-2{margin:.5rem !important}.m-md-3{margin:1rem !important}.m-md-4{margin:1.5rem !important}.m-md-5{margin:3rem !important}.m-md-auto{margin:auto !important}.mx-md-0{margin-right:0 !important;margin-left:0 !important}.mx-md-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-md-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-md-3{margin-right:1rem !important;margin-left:1rem !important}.mx-md-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-md-5{margin-right:3rem !important;margin-left:3rem !important}.mx-md-auto{margin-right:auto !important;margin-left:auto !important}.my-md-0{margin-top:0 !important;margin-bottom:0 !important}.my-md-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-md-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-md-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-md-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-md-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-md-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-md-0{margin-top:0 !important}.mt-md-1{margin-top:.25rem !important}.mt-md-2{margin-top:.5rem !important}.mt-md-3{margin-top:1rem !important}.mt-md-4{margin-top:1.5rem !important}.mt-md-5{margin-top:3rem !important}.mt-md-auto{margin-top:auto !important}.me-md-0{margin-right:0 !important}.me-md-1{margin-right:.25rem !important}.me-md-2{margin-right:.5rem !important}.me-md-3{margin-right:1rem !important}.me-md-4{margin-right:1.5rem !important}.me-md-5{margin-right:3rem !important}.me-md-auto{margin-right:auto !important}.mb-md-0{margin-bottom:0 !important}.mb-md-1{margin-bottom:.25rem !important}.mb-md-2{margin-bottom:.5rem !important}.mb-md-3{margin-bottom:1rem !important}.mb-md-4{margin-bottom:1.5rem !important}.mb-md-5{margin-bottom:3rem !important}.mb-md-auto{margin-bottom:auto !important}.ms-md-0{margin-left:0 !important}.ms-md-1{margin-left:.25rem !important}.ms-md-2{margin-left:.5rem !important}.ms-md-3{margin-left:1rem !important}.ms-md-4{margin-left:1.5rem !important}.ms-md-5{margin-left:3rem !important}.ms-md-auto{margin-left:auto !important}.p-md-0{padding:0 !important}.p-md-1{padding:.25rem !important}.p-md-2{padding:.5rem !important}.p-md-3{padding:1rem !important}.p-md-4{padding:1.5rem !important}.p-md-5{padding:3rem !important}.px-md-0{padding-right:0 !important;padding-left:0 !important}.px-md-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-md-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-md-3{padding-right:1rem !important;padding-left:1rem !important}.px-md-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-md-5{padding-right:3rem !important;padding-left:3rem !important}.py-md-0{padding-top:0 !important;padding-bottom:0 !important}.py-md-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-md-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-md-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-md-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-md-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-md-0{padding-top:0 !important}.pt-md-1{padding-top:.25rem !important}.pt-md-2{padding-top:.5rem !important}.pt-md-3{padding-top:1rem !important}.pt-md-4{padding-top:1.5rem !important}.pt-md-5{padding-top:3rem !important}.pe-md-0{padding-right:0 !important}.pe-md-1{padding-right:.25rem !important}.pe-md-2{padding-right:.5rem !important}.pe-md-3{padding-right:1rem !important}.pe-md-4{padding-right:1.5rem !important}.pe-md-5{padding-right:3rem !important}.pb-md-0{padding-bottom:0 !important}.pb-md-1{padding-bottom:.25rem !important}.pb-md-2{padding-bottom:.5rem !important}.pb-md-3{padding-bottom:1rem !important}.pb-md-4{padding-bottom:1.5rem !important}.pb-md-5{padding-bottom:3rem !important}.ps-md-0{padding-left:0 !important}.ps-md-1{padding-left:.25rem !important}.ps-md-2{padding-left:.5rem !important}.ps-md-3{padding-left:1rem !important}.ps-md-4{padding-left:1.5rem !important}.ps-md-5{padding-left:3rem !important}.gap-md-0{gap:0 !important}.gap-md-1{gap:.25rem !important}.gap-md-2{gap:.5rem !important}.gap-md-3{gap:1rem !important}.gap-md-4{gap:1.5rem !important}.gap-md-5{gap:3rem !important}.row-gap-md-0{row-gap:0 !important}.row-gap-md-1{row-gap:.25rem !important}.row-gap-md-2{row-gap:.5rem !important}.row-gap-md-3{row-gap:1rem !important}.row-gap-md-4{row-gap:1.5rem !important}.row-gap-md-5{row-gap:3rem !important}.column-gap-md-0{column-gap:0 !important}.column-gap-md-1{column-gap:.25rem !important}.column-gap-md-2{column-gap:.5rem !important}.column-gap-md-3{column-gap:1rem !important}.column-gap-md-4{column-gap:1.5rem !important}.column-gap-md-5{column-gap:3rem !important}.text-md-start{text-align:left !important}.text-md-end{text-align:right !important}.text-md-center{text-align:center !important}}@media(min-width: 992px){.float-lg-start{float:left !important}.float-lg-end{float:right !important}.float-lg-none{float:none !important}.object-fit-lg-contain{object-fit:contain !important}.object-fit-lg-cover{object-fit:cover !important}.object-fit-lg-fill{object-fit:fill !important}.object-fit-lg-scale{object-fit:scale-down !important}.object-fit-lg-none{object-fit:none !important}.d-lg-inline{display:inline !important}.d-lg-inline-block{display:inline-block !important}.d-lg-block{display:block !important}.d-lg-grid{display:grid !important}.d-lg-inline-grid{display:inline-grid !important}.d-lg-table{display:table !important}.d-lg-table-row{display:table-row !important}.d-lg-table-cell{display:table-cell !important}.d-lg-flex{display:flex !important}.d-lg-inline-flex{display:inline-flex !important}.d-lg-none{display:none !important}.flex-lg-fill{flex:1 1 auto !important}.flex-lg-row{flex-direction:row !important}.flex-lg-column{flex-direction:column !important}.flex-lg-row-reverse{flex-direction:row-reverse !important}.flex-lg-column-reverse{flex-direction:column-reverse !important}.flex-lg-grow-0{flex-grow:0 !important}.flex-lg-grow-1{flex-grow:1 !important}.flex-lg-shrink-0{flex-shrink:0 !important}.flex-lg-shrink-1{flex-shrink:1 !important}.flex-lg-wrap{flex-wrap:wrap !important}.flex-lg-nowrap{flex-wrap:nowrap !important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-lg-start{justify-content:flex-start !important}.justify-content-lg-end{justify-content:flex-end !important}.justify-content-lg-center{justify-content:center !important}.justify-content-lg-between{justify-content:space-between !important}.justify-content-lg-around{justify-content:space-around !important}.justify-content-lg-evenly{justify-content:space-evenly !important}.align-items-lg-start{align-items:flex-start !important}.align-items-lg-end{align-items:flex-end !important}.align-items-lg-center{align-items:center !important}.align-items-lg-baseline{align-items:baseline !important}.align-items-lg-stretch{align-items:stretch !important}.align-content-lg-start{align-content:flex-start !important}.align-content-lg-end{align-content:flex-end !important}.align-content-lg-center{align-content:center !important}.align-content-lg-between{align-content:space-between !important}.align-content-lg-around{align-content:space-around !important}.align-content-lg-stretch{align-content:stretch !important}.align-self-lg-auto{align-self:auto !important}.align-self-lg-start{align-self:flex-start !important}.align-self-lg-end{align-self:flex-end !important}.align-self-lg-center{align-self:center !important}.align-self-lg-baseline{align-self:baseline !important}.align-self-lg-stretch{align-self:stretch !important}.order-lg-first{order:-1 !important}.order-lg-0{order:0 !important}.order-lg-1{order:1 !important}.order-lg-2{order:2 !important}.order-lg-3{order:3 !important}.order-lg-4{order:4 !important}.order-lg-5{order:5 !important}.order-lg-last{order:6 !important}.m-lg-0{margin:0 !important}.m-lg-1{margin:.25rem !important}.m-lg-2{margin:.5rem !important}.m-lg-3{margin:1rem !important}.m-lg-4{margin:1.5rem !important}.m-lg-5{margin:3rem !important}.m-lg-auto{margin:auto !important}.mx-lg-0{margin-right:0 !important;margin-left:0 !important}.mx-lg-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-lg-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-lg-3{margin-right:1rem !important;margin-left:1rem !important}.mx-lg-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-lg-5{margin-right:3rem !important;margin-left:3rem !important}.mx-lg-auto{margin-right:auto !important;margin-left:auto !important}.my-lg-0{margin-top:0 !important;margin-bottom:0 !important}.my-lg-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-lg-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-lg-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-lg-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-lg-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-lg-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-lg-0{margin-top:0 !important}.mt-lg-1{margin-top:.25rem !important}.mt-lg-2{margin-top:.5rem !important}.mt-lg-3{margin-top:1rem !important}.mt-lg-4{margin-top:1.5rem !important}.mt-lg-5{margin-top:3rem !important}.mt-lg-auto{margin-top:auto !important}.me-lg-0{margin-right:0 !important}.me-lg-1{margin-right:.25rem !important}.me-lg-2{margin-right:.5rem !important}.me-lg-3{margin-right:1rem !important}.me-lg-4{margin-right:1.5rem !important}.me-lg-5{margin-right:3rem !important}.me-lg-auto{margin-right:auto !important}.mb-lg-0{margin-bottom:0 !important}.mb-lg-1{margin-bottom:.25rem !important}.mb-lg-2{margin-bottom:.5rem !important}.mb-lg-3{margin-bottom:1rem !important}.mb-lg-4{margin-bottom:1.5rem !important}.mb-lg-5{margin-bottom:3rem !important}.mb-lg-auto{margin-bottom:auto !important}.ms-lg-0{margin-left:0 !important}.ms-lg-1{margin-left:.25rem !important}.ms-lg-2{margin-left:.5rem !important}.ms-lg-3{margin-left:1rem !important}.ms-lg-4{margin-left:1.5rem !important}.ms-lg-5{margin-left:3rem !important}.ms-lg-auto{margin-left:auto !important}.p-lg-0{padding:0 !important}.p-lg-1{padding:.25rem !important}.p-lg-2{padding:.5rem !important}.p-lg-3{padding:1rem !important}.p-lg-4{padding:1.5rem !important}.p-lg-5{padding:3rem !important}.px-lg-0{padding-right:0 !important;padding-left:0 !important}.px-lg-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-lg-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-lg-3{padding-right:1rem !important;padding-left:1rem !important}.px-lg-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-lg-5{padding-right:3rem !important;padding-left:3rem !important}.py-lg-0{padding-top:0 !important;padding-bottom:0 !important}.py-lg-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-lg-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-lg-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-lg-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-lg-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-lg-0{padding-top:0 !important}.pt-lg-1{padding-top:.25rem !important}.pt-lg-2{padding-top:.5rem !important}.pt-lg-3{padding-top:1rem !important}.pt-lg-4{padding-top:1.5rem !important}.pt-lg-5{padding-top:3rem !important}.pe-lg-0{padding-right:0 !important}.pe-lg-1{padding-right:.25rem !important}.pe-lg-2{padding-right:.5rem !important}.pe-lg-3{padding-right:1rem !important}.pe-lg-4{padding-right:1.5rem !important}.pe-lg-5{padding-right:3rem !important}.pb-lg-0{padding-bottom:0 !important}.pb-lg-1{padding-bottom:.25rem !important}.pb-lg-2{padding-bottom:.5rem !important}.pb-lg-3{padding-bottom:1rem !important}.pb-lg-4{padding-bottom:1.5rem !important}.pb-lg-5{padding-bottom:3rem !important}.ps-lg-0{padding-left:0 !important}.ps-lg-1{padding-left:.25rem !important}.ps-lg-2{padding-left:.5rem !important}.ps-lg-3{padding-left:1rem !important}.ps-lg-4{padding-left:1.5rem !important}.ps-lg-5{padding-left:3rem !important}.gap-lg-0{gap:0 !important}.gap-lg-1{gap:.25rem !important}.gap-lg-2{gap:.5rem !important}.gap-lg-3{gap:1rem !important}.gap-lg-4{gap:1.5rem !important}.gap-lg-5{gap:3rem !important}.row-gap-lg-0{row-gap:0 !important}.row-gap-lg-1{row-gap:.25rem !important}.row-gap-lg-2{row-gap:.5rem !important}.row-gap-lg-3{row-gap:1rem !important}.row-gap-lg-4{row-gap:1.5rem !important}.row-gap-lg-5{row-gap:3rem !important}.column-gap-lg-0{column-gap:0 !important}.column-gap-lg-1{column-gap:.25rem !important}.column-gap-lg-2{column-gap:.5rem !important}.column-gap-lg-3{column-gap:1rem !important}.column-gap-lg-4{column-gap:1.5rem !important}.column-gap-lg-5{column-gap:3rem !important}.text-lg-start{text-align:left !important}.text-lg-end{text-align:right !important}.text-lg-center{text-align:center !important}}@media(min-width: 1200px){.float-xl-start{float:left !important}.float-xl-end{float:right !important}.float-xl-none{float:none !important}.object-fit-xl-contain{object-fit:contain !important}.object-fit-xl-cover{object-fit:cover !important}.object-fit-xl-fill{object-fit:fill !important}.object-fit-xl-scale{object-fit:scale-down !important}.object-fit-xl-none{object-fit:none !important}.d-xl-inline{display:inline !important}.d-xl-inline-block{display:inline-block !important}.d-xl-block{display:block !important}.d-xl-grid{display:grid !important}.d-xl-inline-grid{display:inline-grid !important}.d-xl-table{display:table !important}.d-xl-table-row{display:table-row !important}.d-xl-table-cell{display:table-cell !important}.d-xl-flex{display:flex !important}.d-xl-inline-flex{display:inline-flex !important}.d-xl-none{display:none !important}.flex-xl-fill{flex:1 1 auto !important}.flex-xl-row{flex-direction:row !important}.flex-xl-column{flex-direction:column !important}.flex-xl-row-reverse{flex-direction:row-reverse !important}.flex-xl-column-reverse{flex-direction:column-reverse !important}.flex-xl-grow-0{flex-grow:0 !important}.flex-xl-grow-1{flex-grow:1 !important}.flex-xl-shrink-0{flex-shrink:0 !important}.flex-xl-shrink-1{flex-shrink:1 !important}.flex-xl-wrap{flex-wrap:wrap !important}.flex-xl-nowrap{flex-wrap:nowrap !important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-xl-start{justify-content:flex-start !important}.justify-content-xl-end{justify-content:flex-end !important}.justify-content-xl-center{justify-content:center !important}.justify-content-xl-between{justify-content:space-between !important}.justify-content-xl-around{justify-content:space-around !important}.justify-content-xl-evenly{justify-content:space-evenly !important}.align-items-xl-start{align-items:flex-start !important}.align-items-xl-end{align-items:flex-end !important}.align-items-xl-center{align-items:center !important}.align-items-xl-baseline{align-items:baseline !important}.align-items-xl-stretch{align-items:stretch !important}.align-content-xl-start{align-content:flex-start !important}.align-content-xl-end{align-content:flex-end !important}.align-content-xl-center{align-content:center !important}.align-content-xl-between{align-content:space-between !important}.align-content-xl-around{align-content:space-around !important}.align-content-xl-stretch{align-content:stretch !important}.align-self-xl-auto{align-self:auto !important}.align-self-xl-start{align-self:flex-start !important}.align-self-xl-end{align-self:flex-end !important}.align-self-xl-center{align-self:center !important}.align-self-xl-baseline{align-self:baseline !important}.align-self-xl-stretch{align-self:stretch !important}.order-xl-first{order:-1 !important}.order-xl-0{order:0 !important}.order-xl-1{order:1 !important}.order-xl-2{order:2 !important}.order-xl-3{order:3 !important}.order-xl-4{order:4 !important}.order-xl-5{order:5 !important}.order-xl-last{order:6 !important}.m-xl-0{margin:0 !important}.m-xl-1{margin:.25rem !important}.m-xl-2{margin:.5rem !important}.m-xl-3{margin:1rem !important}.m-xl-4{margin:1.5rem !important}.m-xl-5{margin:3rem !important}.m-xl-auto{margin:auto !important}.mx-xl-0{margin-right:0 !important;margin-left:0 !important}.mx-xl-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-xl-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-xl-3{margin-right:1rem !important;margin-left:1rem !important}.mx-xl-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-xl-5{margin-right:3rem !important;margin-left:3rem !important}.mx-xl-auto{margin-right:auto !important;margin-left:auto !important}.my-xl-0{margin-top:0 !important;margin-bottom:0 !important}.my-xl-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-xl-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-xl-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-xl-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-xl-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-xl-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-xl-0{margin-top:0 !important}.mt-xl-1{margin-top:.25rem !important}.mt-xl-2{margin-top:.5rem !important}.mt-xl-3{margin-top:1rem !important}.mt-xl-4{margin-top:1.5rem !important}.mt-xl-5{margin-top:3rem !important}.mt-xl-auto{margin-top:auto !important}.me-xl-0{margin-right:0 !important}.me-xl-1{margin-right:.25rem !important}.me-xl-2{margin-right:.5rem !important}.me-xl-3{margin-right:1rem !important}.me-xl-4{margin-right:1.5rem !important}.me-xl-5{margin-right:3rem !important}.me-xl-auto{margin-right:auto !important}.mb-xl-0{margin-bottom:0 !important}.mb-xl-1{margin-bottom:.25rem !important}.mb-xl-2{margin-bottom:.5rem !important}.mb-xl-3{margin-bottom:1rem !important}.mb-xl-4{margin-bottom:1.5rem !important}.mb-xl-5{margin-bottom:3rem !important}.mb-xl-auto{margin-bottom:auto !important}.ms-xl-0{margin-left:0 !important}.ms-xl-1{margin-left:.25rem !important}.ms-xl-2{margin-left:.5rem !important}.ms-xl-3{margin-left:1rem !important}.ms-xl-4{margin-left:1.5rem !important}.ms-xl-5{margin-left:3rem !important}.ms-xl-auto{margin-left:auto !important}.p-xl-0{padding:0 !important}.p-xl-1{padding:.25rem !important}.p-xl-2{padding:.5rem !important}.p-xl-3{padding:1rem !important}.p-xl-4{padding:1.5rem !important}.p-xl-5{padding:3rem !important}.px-xl-0{padding-right:0 !important;padding-left:0 !important}.px-xl-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-xl-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-xl-3{padding-right:1rem !important;padding-left:1rem !important}.px-xl-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-xl-5{padding-right:3rem !important;padding-left:3rem !important}.py-xl-0{padding-top:0 !important;padding-bottom:0 !important}.py-xl-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-xl-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-xl-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-xl-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-xl-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-xl-0{padding-top:0 !important}.pt-xl-1{padding-top:.25rem !important}.pt-xl-2{padding-top:.5rem !important}.pt-xl-3{padding-top:1rem !important}.pt-xl-4{padding-top:1.5rem !important}.pt-xl-5{padding-top:3rem !important}.pe-xl-0{padding-right:0 !important}.pe-xl-1{padding-right:.25rem !important}.pe-xl-2{padding-right:.5rem !important}.pe-xl-3{padding-right:1rem !important}.pe-xl-4{padding-right:1.5rem !important}.pe-xl-5{padding-right:3rem !important}.pb-xl-0{padding-bottom:0 !important}.pb-xl-1{padding-bottom:.25rem !important}.pb-xl-2{padding-bottom:.5rem !important}.pb-xl-3{padding-bottom:1rem !important}.pb-xl-4{padding-bottom:1.5rem !important}.pb-xl-5{padding-bottom:3rem !important}.ps-xl-0{padding-left:0 !important}.ps-xl-1{padding-left:.25rem !important}.ps-xl-2{padding-left:.5rem !important}.ps-xl-3{padding-left:1rem !important}.ps-xl-4{padding-left:1.5rem !important}.ps-xl-5{padding-left:3rem !important}.gap-xl-0{gap:0 !important}.gap-xl-1{gap:.25rem !important}.gap-xl-2{gap:.5rem !important}.gap-xl-3{gap:1rem !important}.gap-xl-4{gap:1.5rem !important}.gap-xl-5{gap:3rem !important}.row-gap-xl-0{row-gap:0 !important}.row-gap-xl-1{row-gap:.25rem !important}.row-gap-xl-2{row-gap:.5rem !important}.row-gap-xl-3{row-gap:1rem !important}.row-gap-xl-4{row-gap:1.5rem !important}.row-gap-xl-5{row-gap:3rem !important}.column-gap-xl-0{column-gap:0 !important}.column-gap-xl-1{column-gap:.25rem !important}.column-gap-xl-2{column-gap:.5rem !important}.column-gap-xl-3{column-gap:1rem !important}.column-gap-xl-4{column-gap:1.5rem !important}.column-gap-xl-5{column-gap:3rem !important}.text-xl-start{text-align:left !important}.text-xl-end{text-align:right !important}.text-xl-center{text-align:center !important}}@media(min-width: 1400px){.float-xxl-start{float:left !important}.float-xxl-end{float:right !important}.float-xxl-none{float:none !important}.object-fit-xxl-contain{object-fit:contain !important}.object-fit-xxl-cover{object-fit:cover !important}.object-fit-xxl-fill{object-fit:fill !important}.object-fit-xxl-scale{object-fit:scale-down !important}.object-fit-xxl-none{object-fit:none !important}.d-xxl-inline{display:inline !important}.d-xxl-inline-block{display:inline-block !important}.d-xxl-block{display:block !important}.d-xxl-grid{display:grid !important}.d-xxl-inline-grid{display:inline-grid !important}.d-xxl-table{display:table !important}.d-xxl-table-row{display:table-row !important}.d-xxl-table-cell{display:table-cell !important}.d-xxl-flex{display:flex !important}.d-xxl-inline-flex{display:inline-flex !important}.d-xxl-none{display:none !important}.flex-xxl-fill{flex:1 1 auto !important}.flex-xxl-row{flex-direction:row !important}.flex-xxl-column{flex-direction:column !important}.flex-xxl-row-reverse{flex-direction:row-reverse !important}.flex-xxl-column-reverse{flex-direction:column-reverse !important}.flex-xxl-grow-0{flex-grow:0 !important}.flex-xxl-grow-1{flex-grow:1 !important}.flex-xxl-shrink-0{flex-shrink:0 !important}.flex-xxl-shrink-1{flex-shrink:1 !important}.flex-xxl-wrap{flex-wrap:wrap !important}.flex-xxl-nowrap{flex-wrap:nowrap !important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-xxl-start{justify-content:flex-start !important}.justify-content-xxl-end{justify-content:flex-end !important}.justify-content-xxl-center{justify-content:center !important}.justify-content-xxl-between{justify-content:space-between !important}.justify-content-xxl-around{justify-content:space-around !important}.justify-content-xxl-evenly{justify-content:space-evenly !important}.align-items-xxl-start{align-items:flex-start !important}.align-items-xxl-end{align-items:flex-end !important}.align-items-xxl-center{align-items:center !important}.align-items-xxl-baseline{align-items:baseline !important}.align-items-xxl-stretch{align-items:stretch !important}.align-content-xxl-start{align-content:flex-start !important}.align-content-xxl-end{align-content:flex-end !important}.align-content-xxl-center{align-content:center !important}.align-content-xxl-between{align-content:space-between !important}.align-content-xxl-around{align-content:space-around !important}.align-content-xxl-stretch{align-content:stretch !important}.align-self-xxl-auto{align-self:auto !important}.align-self-xxl-start{align-self:flex-start !important}.align-self-xxl-end{align-self:flex-end !important}.align-self-xxl-center{align-self:center !important}.align-self-xxl-baseline{align-self:baseline !important}.align-self-xxl-stretch{align-self:stretch !important}.order-xxl-first{order:-1 !important}.order-xxl-0{order:0 !important}.order-xxl-1{order:1 !important}.order-xxl-2{order:2 !important}.order-xxl-3{order:3 !important}.order-xxl-4{order:4 !important}.order-xxl-5{order:5 !important}.order-xxl-last{order:6 !important}.m-xxl-0{margin:0 !important}.m-xxl-1{margin:.25rem !important}.m-xxl-2{margin:.5rem !important}.m-xxl-3{margin:1rem !important}.m-xxl-4{margin:1.5rem !important}.m-xxl-5{margin:3rem !important}.m-xxl-auto{margin:auto !important}.mx-xxl-0{margin-right:0 !important;margin-left:0 !important}.mx-xxl-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-xxl-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-xxl-3{margin-right:1rem !important;margin-left:1rem !important}.mx-xxl-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-xxl-5{margin-right:3rem !important;margin-left:3rem !important}.mx-xxl-auto{margin-right:auto !important;margin-left:auto !important}.my-xxl-0{margin-top:0 !important;margin-bottom:0 !important}.my-xxl-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-xxl-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-xxl-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-xxl-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-xxl-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-xxl-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-xxl-0{margin-top:0 !important}.mt-xxl-1{margin-top:.25rem !important}.mt-xxl-2{margin-top:.5rem !important}.mt-xxl-3{margin-top:1rem !important}.mt-xxl-4{margin-top:1.5rem !important}.mt-xxl-5{margin-top:3rem !important}.mt-xxl-auto{margin-top:auto !important}.me-xxl-0{margin-right:0 !important}.me-xxl-1{margin-right:.25rem !important}.me-xxl-2{margin-right:.5rem !important}.me-xxl-3{margin-right:1rem !important}.me-xxl-4{margin-right:1.5rem !important}.me-xxl-5{margin-right:3rem !important}.me-xxl-auto{margin-right:auto !important}.mb-xxl-0{margin-bottom:0 !important}.mb-xxl-1{margin-bottom:.25rem !important}.mb-xxl-2{margin-bottom:.5rem !important}.mb-xxl-3{margin-bottom:1rem !important}.mb-xxl-4{margin-bottom:1.5rem !important}.mb-xxl-5{margin-bottom:3rem !important}.mb-xxl-auto{margin-bottom:auto !important}.ms-xxl-0{margin-left:0 !important}.ms-xxl-1{margin-left:.25rem !important}.ms-xxl-2{margin-left:.5rem !important}.ms-xxl-3{margin-left:1rem !important}.ms-xxl-4{margin-left:1.5rem !important}.ms-xxl-5{margin-left:3rem !important}.ms-xxl-auto{margin-left:auto !important}.p-xxl-0{padding:0 !important}.p-xxl-1{padding:.25rem !important}.p-xxl-2{padding:.5rem !important}.p-xxl-3{padding:1rem !important}.p-xxl-4{padding:1.5rem !important}.p-xxl-5{padding:3rem !important}.px-xxl-0{padding-right:0 !important;padding-left:0 !important}.px-xxl-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-xxl-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-xxl-3{padding-right:1rem !important;padding-left:1rem !important}.px-xxl-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-xxl-5{padding-right:3rem !important;padding-left:3rem !important}.py-xxl-0{padding-top:0 !important;padding-bottom:0 !important}.py-xxl-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-xxl-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-xxl-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-xxl-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-xxl-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-xxl-0{padding-top:0 !important}.pt-xxl-1{padding-top:.25rem !important}.pt-xxl-2{padding-top:.5rem !important}.pt-xxl-3{padding-top:1rem !important}.pt-xxl-4{padding-top:1.5rem !important}.pt-xxl-5{padding-top:3rem !important}.pe-xxl-0{padding-right:0 !important}.pe-xxl-1{padding-right:.25rem !important}.pe-xxl-2{padding-right:.5rem !important}.pe-xxl-3{padding-right:1rem !important}.pe-xxl-4{padding-right:1.5rem !important}.pe-xxl-5{padding-right:3rem !important}.pb-xxl-0{padding-bottom:0 !important}.pb-xxl-1{padding-bottom:.25rem !important}.pb-xxl-2{padding-bottom:.5rem !important}.pb-xxl-3{padding-bottom:1rem !important}.pb-xxl-4{padding-bottom:1.5rem !important}.pb-xxl-5{padding-bottom:3rem !important}.ps-xxl-0{padding-left:0 !important}.ps-xxl-1{padding-left:.25rem !important}.ps-xxl-2{padding-left:.5rem !important}.ps-xxl-3{padding-left:1rem !important}.ps-xxl-4{padding-left:1.5rem !important}.ps-xxl-5{padding-left:3rem !important}.gap-xxl-0{gap:0 !important}.gap-xxl-1{gap:.25rem !important}.gap-xxl-2{gap:.5rem !important}.gap-xxl-3{gap:1rem !important}.gap-xxl-4{gap:1.5rem !important}.gap-xxl-5{gap:3rem !important}.row-gap-xxl-0{row-gap:0 !important}.row-gap-xxl-1{row-gap:.25rem !important}.row-gap-xxl-2{row-gap:.5rem !important}.row-gap-xxl-3{row-gap:1rem !important}.row-gap-xxl-4{row-gap:1.5rem !important}.row-gap-xxl-5{row-gap:3rem !important}.column-gap-xxl-0{column-gap:0 !important}.column-gap-xxl-1{column-gap:.25rem !important}.column-gap-xxl-2{column-gap:.5rem !important}.column-gap-xxl-3{column-gap:1rem !important}.column-gap-xxl-4{column-gap:1.5rem !important}.column-gap-xxl-5{column-gap:3rem !important}.text-xxl-start{text-align:left !important}.text-xxl-end{text-align:right !important}.text-xxl-center{text-align:center !important}}.bg-default{color:#fff}.bg-primary{color:#000}.bg-secondary{color:#fff}.bg-success{color:#fff}.bg-info{color:#fff}.bg-warning{color:#fff}.bg-danger{color:#fff}.bg-light{color:#000}.bg-dark{color:#fff}@media(min-width: 1200px){.fs-1{font-size:2rem !important}.fs-2{font-size:1.65rem !important}.fs-3{font-size:1.45rem !important}}@media print{.d-print-inline{display:inline !important}.d-print-inline-block{display:inline-block !important}.d-print-block{display:block !important}.d-print-grid{display:grid !important}.d-print-inline-grid{display:inline-grid !important}.d-print-table{display:table !important}.d-print-table-row{display:table-row !important}.d-print-table-cell{display:table-cell !important}.d-print-flex{display:flex !important}.d-print-inline-flex{display:inline-flex !important}.d-print-none{display:none !important}}:root{--bslib-spacer: 1rem;--bslib-mb-spacer: var(--bslib-spacer, 1rem)}.bslib-mb-spacing{margin-bottom:var(--bslib-mb-spacer)}.bslib-gap-spacing{gap:var(--bslib-mb-spacer)}.bslib-gap-spacing>.bslib-mb-spacing,.bslib-gap-spacing>.form-group,.bslib-gap-spacing>p,.bslib-gap-spacing>pre{margin-bottom:0}.html-fill-container>.html-fill-item.bslib-mb-spacing{margin-bottom:0}.tab-content>.tab-pane.html-fill-container{display:none}.tab-content>.active.html-fill-container{display:flex}.tab-content.html-fill-container{padding:0}.bg-blue{--bslib-color-bg: #2780e3;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-blue{--bslib-color-fg: #2780e3;color:var(--bslib-color-fg)}.bg-indigo{--bslib-color-bg: #6610f2;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-indigo{--bslib-color-fg: #6610f2;color:var(--bslib-color-fg)}.bg-purple{--bslib-color-bg: #613d7c;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-purple{--bslib-color-fg: #613d7c;color:var(--bslib-color-fg)}.bg-pink{--bslib-color-bg: #e83e8c;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-pink{--bslib-color-fg: #e83e8c;color:var(--bslib-color-fg)}.bg-red{--bslib-color-bg: #ff0039;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-red{--bslib-color-fg: #ff0039;color:var(--bslib-color-fg)}.bg-orange{--bslib-color-bg: #f0ad4e;--bslib-color-fg: #000;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-orange{--bslib-color-fg: #f0ad4e;color:var(--bslib-color-fg)}.bg-yellow{--bslib-color-bg: #ff7518;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-yellow{--bslib-color-fg: #ff7518;color:var(--bslib-color-fg)}.bg-green{--bslib-color-bg: #3fb618;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-green{--bslib-color-fg: #3fb618;color:var(--bslib-color-fg)}.bg-teal{--bslib-color-bg: #20c997;--bslib-color-fg: #000;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-teal{--bslib-color-fg: #20c997;color:var(--bslib-color-fg)}.bg-cyan{--bslib-color-bg: #9954bb;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-cyan{--bslib-color-fg: #9954bb;color:var(--bslib-color-fg)}.text-default{--bslib-color-fg: #343a40}.bg-default{--bslib-color-bg: #343a40;--bslib-color-fg: #fff}.text-primary{--bslib-color-fg: #E8E8FC}.bg-primary{--bslib-color-bg: #E8E8FC;--bslib-color-fg: #000}.text-secondary{--bslib-color-fg: #343a40}.bg-secondary{--bslib-color-bg: #343a40;--bslib-color-fg: #fff}.text-success{--bslib-color-fg: #3fb618}.bg-success{--bslib-color-bg: #3fb618;--bslib-color-fg: #fff}.text-info{--bslib-color-fg: #9954bb}.bg-info{--bslib-color-bg: #9954bb;--bslib-color-fg: #fff}.text-warning{--bslib-color-fg: #ff7518}.bg-warning{--bslib-color-bg: #ff7518;--bslib-color-fg: #fff}.text-danger{--bslib-color-fg: #ff0039}.bg-danger{--bslib-color-bg: #ff0039;--bslib-color-fg: #fff}.text-light{--bslib-color-fg: #f8f9fa}.bg-light{--bslib-color-bg: #f8f9fa;--bslib-color-fg: #000}.text-dark{--bslib-color-fg: #343a40}.bg-dark{--bslib-color-bg: #343a40;--bslib-color-fg: #fff}.bg-gradient-blue-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #4053e9;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #4053e9;color:#fff}.bg-gradient-blue-purple{--bslib-color-fg: #fff;--bslib-color-bg: #3e65ba;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #3e65ba;color:#fff}.bg-gradient-blue-pink{--bslib-color-fg: #fff;--bslib-color-bg: #7466c0;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #7466c0;color:#fff}.bg-gradient-blue-red{--bslib-color-fg: #fff;--bslib-color-bg: #7d4d9f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #7d4d9f;color:#fff}.bg-gradient-blue-orange{--bslib-color-fg: #fff;--bslib-color-bg: #7792a7;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #7792a7;color:#fff}.bg-gradient-blue-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #7d7c92;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #7d7c92;color:#fff}.bg-gradient-blue-green{--bslib-color-fg: #fff;--bslib-color-bg: #319692;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #319692;color:#fff}.bg-gradient-blue-teal{--bslib-color-fg: #fff;--bslib-color-bg: #249dc5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #249dc5;color:#fff}.bg-gradient-blue-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #556ed3;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #556ed3;color:#fff}.bg-gradient-indigo-blue{--bslib-color-fg: #fff;--bslib-color-bg: #4d3dec;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #4d3dec;color:#fff}.bg-gradient-indigo-purple{--bslib-color-fg: #fff;--bslib-color-bg: #6422c3;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #6422c3;color:#fff}.bg-gradient-indigo-pink{--bslib-color-fg: #fff;--bslib-color-bg: #9a22c9;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #9a22c9;color:#fff}.bg-gradient-indigo-red{--bslib-color-fg: #fff;--bslib-color-bg: #a30aa8;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #a30aa8;color:#fff}.bg-gradient-indigo-orange{--bslib-color-fg: #fff;--bslib-color-bg: #9d4fb0;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #9d4fb0;color:#fff}.bg-gradient-indigo-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #a3389b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #a3389b;color:#fff}.bg-gradient-indigo-green{--bslib-color-fg: #fff;--bslib-color-bg: #56529b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #56529b;color:#fff}.bg-gradient-indigo-teal{--bslib-color-fg: #fff;--bslib-color-bg: #4a5ace;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #4a5ace;color:#fff}.bg-gradient-indigo-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #7a2bdc;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #7a2bdc;color:#fff}.bg-gradient-purple-blue{--bslib-color-fg: #fff;--bslib-color-bg: #4a58a5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #4a58a5;color:#fff}.bg-gradient-purple-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #632bab;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #632bab;color:#fff}.bg-gradient-purple-pink{--bslib-color-fg: #fff;--bslib-color-bg: #973d82;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #973d82;color:#fff}.bg-gradient-purple-red{--bslib-color-fg: #fff;--bslib-color-bg: #a02561;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #a02561;color:#fff}.bg-gradient-purple-orange{--bslib-color-fg: #fff;--bslib-color-bg: #9a6a6a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #9a6a6a;color:#fff}.bg-gradient-purple-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #a05354;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #a05354;color:#fff}.bg-gradient-purple-green{--bslib-color-fg: #fff;--bslib-color-bg: #536d54;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #536d54;color:#fff}.bg-gradient-purple-teal{--bslib-color-fg: #fff;--bslib-color-bg: #477587;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #477587;color:#fff}.bg-gradient-purple-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #774695;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #774695;color:#fff}.bg-gradient-pink-blue{--bslib-color-fg: #fff;--bslib-color-bg: #9b58af;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #9b58af;color:#fff}.bg-gradient-pink-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #b42cb5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #b42cb5;color:#fff}.bg-gradient-pink-purple{--bslib-color-fg: #fff;--bslib-color-bg: #b23e86;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #b23e86;color:#fff}.bg-gradient-pink-red{--bslib-color-fg: #fff;--bslib-color-bg: #f1256b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #f1256b;color:#fff}.bg-gradient-pink-orange{--bslib-color-fg: #fff;--bslib-color-bg: #eb6a73;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #eb6a73;color:#fff}.bg-gradient-pink-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #f1545e;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #f1545e;color:#fff}.bg-gradient-pink-green{--bslib-color-fg: #fff;--bslib-color-bg: #a46e5e;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #a46e5e;color:#fff}.bg-gradient-pink-teal{--bslib-color-fg: #fff;--bslib-color-bg: #987690;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #987690;color:#fff}.bg-gradient-pink-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #c8479f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #c8479f;color:#fff}.bg-gradient-red-blue{--bslib-color-fg: #fff;--bslib-color-bg: #a9337d;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #a9337d;color:#fff}.bg-gradient-red-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #c20683;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #c20683;color:#fff}.bg-gradient-red-purple{--bslib-color-fg: #fff;--bslib-color-bg: #c01854;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #c01854;color:#fff}.bg-gradient-red-pink{--bslib-color-fg: #fff;--bslib-color-bg: #f6195a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #f6195a;color:#fff}.bg-gradient-red-orange{--bslib-color-fg: #fff;--bslib-color-bg: #f94541;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #f94541;color:#fff}.bg-gradient-red-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #ff2f2c;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #ff2f2c;color:#fff}.bg-gradient-red-green{--bslib-color-fg: #fff;--bslib-color-bg: #b2492c;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #b2492c;color:#fff}.bg-gradient-red-teal{--bslib-color-fg: #fff;--bslib-color-bg: #a6505f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #a6505f;color:#fff}.bg-gradient-red-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #d6226d;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #d6226d;color:#fff}.bg-gradient-orange-blue{--bslib-color-fg: #fff;--bslib-color-bg: #a09b8a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #a09b8a;color:#fff}.bg-gradient-orange-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #b96e90;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #b96e90;color:#fff}.bg-gradient-orange-purple{--bslib-color-fg: #fff;--bslib-color-bg: #b78060;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #b78060;color:#fff}.bg-gradient-orange-pink{--bslib-color-fg: #fff;--bslib-color-bg: #ed8167;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #ed8167;color:#fff}.bg-gradient-orange-red{--bslib-color-fg: #fff;--bslib-color-bg: #f66846;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #f66846;color:#fff}.bg-gradient-orange-yellow{--bslib-color-fg: #000;--bslib-color-bg: #f69738;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #f69738;color:#000}.bg-gradient-orange-green{--bslib-color-fg: #000;--bslib-color-bg: #a9b138;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #a9b138;color:#000}.bg-gradient-orange-teal{--bslib-color-fg: #000;--bslib-color-bg: #9db86b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #9db86b;color:#000}.bg-gradient-orange-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #cd897a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #cd897a;color:#fff}.bg-gradient-yellow-blue{--bslib-color-fg: #fff;--bslib-color-bg: #a97969;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #a97969;color:#fff}.bg-gradient-yellow-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #c24d6f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #c24d6f;color:#fff}.bg-gradient-yellow-purple{--bslib-color-fg: #fff;--bslib-color-bg: #c05f40;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #c05f40;color:#fff}.bg-gradient-yellow-pink{--bslib-color-fg: #fff;--bslib-color-bg: #f65f46;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #f65f46;color:#fff}.bg-gradient-yellow-red{--bslib-color-fg: #fff;--bslib-color-bg: #ff4625;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #ff4625;color:#fff}.bg-gradient-yellow-orange{--bslib-color-fg: #000;--bslib-color-bg: #f98b2e;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #f98b2e;color:#000}.bg-gradient-yellow-green{--bslib-color-fg: #fff;--bslib-color-bg: #b28f18;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #b28f18;color:#fff}.bg-gradient-yellow-teal{--bslib-color-fg: #fff;--bslib-color-bg: #a6974b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #a6974b;color:#fff}.bg-gradient-yellow-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #d66859;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #d66859;color:#fff}.bg-gradient-green-blue{--bslib-color-fg: #fff;--bslib-color-bg: #35a069;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #35a069;color:#fff}.bg-gradient-green-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #4f746f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #4f746f;color:#fff}.bg-gradient-green-purple{--bslib-color-fg: #fff;--bslib-color-bg: #4d8640;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #4d8640;color:#fff}.bg-gradient-green-pink{--bslib-color-fg: #fff;--bslib-color-bg: #838646;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #838646;color:#fff}.bg-gradient-green-red{--bslib-color-fg: #fff;--bslib-color-bg: #8c6d25;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #8c6d25;color:#fff}.bg-gradient-green-orange{--bslib-color-fg: #000;--bslib-color-bg: #86b22e;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #86b22e;color:#000}.bg-gradient-green-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #8c9c18;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #8c9c18;color:#fff}.bg-gradient-green-teal{--bslib-color-fg: #000;--bslib-color-bg: #33be4b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #33be4b;color:#000}.bg-gradient-green-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #638f59;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #638f59;color:#fff}.bg-gradient-teal-blue{--bslib-color-fg: #fff;--bslib-color-bg: #23acb5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #23acb5;color:#fff}.bg-gradient-teal-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #3c7fbb;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #3c7fbb;color:#fff}.bg-gradient-teal-purple{--bslib-color-fg: #fff;--bslib-color-bg: #3a918c;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #3a918c;color:#fff}.bg-gradient-teal-pink{--bslib-color-fg: #fff;--bslib-color-bg: #709193;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #709193;color:#fff}.bg-gradient-teal-red{--bslib-color-fg: #fff;--bslib-color-bg: #797971;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #797971;color:#fff}.bg-gradient-teal-orange{--bslib-color-fg: #000;--bslib-color-bg: #73be7a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #73be7a;color:#000}.bg-gradient-teal-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #79a764;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #79a764;color:#fff}.bg-gradient-teal-green{--bslib-color-fg: #000;--bslib-color-bg: #2cc164;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #2cc164;color:#000}.bg-gradient-teal-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #509aa5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #509aa5;color:#fff}.bg-gradient-cyan-blue{--bslib-color-fg: #fff;--bslib-color-bg: #6b66cb;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #6b66cb;color:#fff}.bg-gradient-cyan-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #8539d1;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #8539d1;color:#fff}.bg-gradient-cyan-purple{--bslib-color-fg: #fff;--bslib-color-bg: #834ba2;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #834ba2;color:#fff}.bg-gradient-cyan-pink{--bslib-color-fg: #fff;--bslib-color-bg: #b94ba8;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #b94ba8;color:#fff}.bg-gradient-cyan-red{--bslib-color-fg: #fff;--bslib-color-bg: #c23287;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #c23287;color:#fff}.bg-gradient-cyan-orange{--bslib-color-fg: #fff;--bslib-color-bg: #bc788f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #bc788f;color:#fff}.bg-gradient-cyan-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #c2617a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #c2617a;color:#fff}.bg-gradient-cyan-green{--bslib-color-fg: #fff;--bslib-color-bg: #757b7a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #757b7a;color:#fff}.bg-gradient-cyan-teal{--bslib-color-fg: #fff;--bslib-color-bg: #6983ad;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #6983ad;color:#fff}.bg-blue{--bslib-color-bg: #2780e3;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-blue{--bslib-color-fg: #2780e3;color:var(--bslib-color-fg)}.bg-indigo{--bslib-color-bg: #6610f2;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-indigo{--bslib-color-fg: #6610f2;color:var(--bslib-color-fg)}.bg-purple{--bslib-color-bg: #613d7c;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-purple{--bslib-color-fg: #613d7c;color:var(--bslib-color-fg)}.bg-pink{--bslib-color-bg: #e83e8c;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-pink{--bslib-color-fg: #e83e8c;color:var(--bslib-color-fg)}.bg-red{--bslib-color-bg: #ff0039;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-red{--bslib-color-fg: #ff0039;color:var(--bslib-color-fg)}.bg-orange{--bslib-color-bg: #f0ad4e;--bslib-color-fg: #000;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-orange{--bslib-color-fg: #f0ad4e;color:var(--bslib-color-fg)}.bg-yellow{--bslib-color-bg: #ff7518;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-yellow{--bslib-color-fg: #ff7518;color:var(--bslib-color-fg)}.bg-green{--bslib-color-bg: #3fb618;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-green{--bslib-color-fg: #3fb618;color:var(--bslib-color-fg)}.bg-teal{--bslib-color-bg: #20c997;--bslib-color-fg: #000;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-teal{--bslib-color-fg: #20c997;color:var(--bslib-color-fg)}.bg-cyan{--bslib-color-bg: #9954bb;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-cyan{--bslib-color-fg: #9954bb;color:var(--bslib-color-fg)}.text-default{--bslib-color-fg: #343a40}.bg-default{--bslib-color-bg: #343a40;--bslib-color-fg: #fff}.text-primary{--bslib-color-fg: #E8E8FC}.bg-primary{--bslib-color-bg: #E8E8FC;--bslib-color-fg: #000}.text-secondary{--bslib-color-fg: #343a40}.bg-secondary{--bslib-color-bg: #343a40;--bslib-color-fg: #fff}.text-success{--bslib-color-fg: #3fb618}.bg-success{--bslib-color-bg: #3fb618;--bslib-color-fg: #fff}.text-info{--bslib-color-fg: #9954bb}.bg-info{--bslib-color-bg: #9954bb;--bslib-color-fg: #fff}.text-warning{--bslib-color-fg: #ff7518}.bg-warning{--bslib-color-bg: #ff7518;--bslib-color-fg: #fff}.text-danger{--bslib-color-fg: #ff0039}.bg-danger{--bslib-color-bg: #ff0039;--bslib-color-fg: #fff}.text-light{--bslib-color-fg: #f8f9fa}.bg-light{--bslib-color-bg: #f8f9fa;--bslib-color-fg: #000}.text-dark{--bslib-color-fg: #343a40}.bg-dark{--bslib-color-bg: #343a40;--bslib-color-fg: #fff}.bg-gradient-blue-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #4053e9;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #4053e9;color:#fff}.bg-gradient-blue-purple{--bslib-color-fg: #fff;--bslib-color-bg: #3e65ba;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #3e65ba;color:#fff}.bg-gradient-blue-pink{--bslib-color-fg: #fff;--bslib-color-bg: #7466c0;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #7466c0;color:#fff}.bg-gradient-blue-red{--bslib-color-fg: #fff;--bslib-color-bg: #7d4d9f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #7d4d9f;color:#fff}.bg-gradient-blue-orange{--bslib-color-fg: #fff;--bslib-color-bg: #7792a7;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #7792a7;color:#fff}.bg-gradient-blue-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #7d7c92;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #7d7c92;color:#fff}.bg-gradient-blue-green{--bslib-color-fg: #fff;--bslib-color-bg: #319692;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #319692;color:#fff}.bg-gradient-blue-teal{--bslib-color-fg: #fff;--bslib-color-bg: #249dc5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #249dc5;color:#fff}.bg-gradient-blue-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #556ed3;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #556ed3;color:#fff}.bg-gradient-indigo-blue{--bslib-color-fg: #fff;--bslib-color-bg: #4d3dec;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #4d3dec;color:#fff}.bg-gradient-indigo-purple{--bslib-color-fg: #fff;--bslib-color-bg: #6422c3;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #6422c3;color:#fff}.bg-gradient-indigo-pink{--bslib-color-fg: #fff;--bslib-color-bg: #9a22c9;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #9a22c9;color:#fff}.bg-gradient-indigo-red{--bslib-color-fg: #fff;--bslib-color-bg: #a30aa8;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #a30aa8;color:#fff}.bg-gradient-indigo-orange{--bslib-color-fg: #fff;--bslib-color-bg: #9d4fb0;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #9d4fb0;color:#fff}.bg-gradient-indigo-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #a3389b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #a3389b;color:#fff}.bg-gradient-indigo-green{--bslib-color-fg: #fff;--bslib-color-bg: #56529b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #56529b;color:#fff}.bg-gradient-indigo-teal{--bslib-color-fg: #fff;--bslib-color-bg: #4a5ace;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #4a5ace;color:#fff}.bg-gradient-indigo-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #7a2bdc;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #7a2bdc;color:#fff}.bg-gradient-purple-blue{--bslib-color-fg: #fff;--bslib-color-bg: #4a58a5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #4a58a5;color:#fff}.bg-gradient-purple-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #632bab;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #632bab;color:#fff}.bg-gradient-purple-pink{--bslib-color-fg: #fff;--bslib-color-bg: #973d82;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #973d82;color:#fff}.bg-gradient-purple-red{--bslib-color-fg: #fff;--bslib-color-bg: #a02561;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #a02561;color:#fff}.bg-gradient-purple-orange{--bslib-color-fg: #fff;--bslib-color-bg: #9a6a6a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #9a6a6a;color:#fff}.bg-gradient-purple-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #a05354;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #a05354;color:#fff}.bg-gradient-purple-green{--bslib-color-fg: #fff;--bslib-color-bg: #536d54;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #536d54;color:#fff}.bg-gradient-purple-teal{--bslib-color-fg: #fff;--bslib-color-bg: #477587;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #477587;color:#fff}.bg-gradient-purple-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #774695;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #774695;color:#fff}.bg-gradient-pink-blue{--bslib-color-fg: #fff;--bslib-color-bg: #9b58af;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #9b58af;color:#fff}.bg-gradient-pink-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #b42cb5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #b42cb5;color:#fff}.bg-gradient-pink-purple{--bslib-color-fg: #fff;--bslib-color-bg: #b23e86;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #b23e86;color:#fff}.bg-gradient-pink-red{--bslib-color-fg: #fff;--bslib-color-bg: #f1256b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #f1256b;color:#fff}.bg-gradient-pink-orange{--bslib-color-fg: #fff;--bslib-color-bg: #eb6a73;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #eb6a73;color:#fff}.bg-gradient-pink-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #f1545e;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #f1545e;color:#fff}.bg-gradient-pink-green{--bslib-color-fg: #fff;--bslib-color-bg: #a46e5e;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #a46e5e;color:#fff}.bg-gradient-pink-teal{--bslib-color-fg: #fff;--bslib-color-bg: #987690;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #987690;color:#fff}.bg-gradient-pink-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #c8479f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #c8479f;color:#fff}.bg-gradient-red-blue{--bslib-color-fg: #fff;--bslib-color-bg: #a9337d;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #a9337d;color:#fff}.bg-gradient-red-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #c20683;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #c20683;color:#fff}.bg-gradient-red-purple{--bslib-color-fg: #fff;--bslib-color-bg: #c01854;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #c01854;color:#fff}.bg-gradient-red-pink{--bslib-color-fg: #fff;--bslib-color-bg: #f6195a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #f6195a;color:#fff}.bg-gradient-red-orange{--bslib-color-fg: #fff;--bslib-color-bg: #f94541;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #f94541;color:#fff}.bg-gradient-red-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #ff2f2c;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #ff2f2c;color:#fff}.bg-gradient-red-green{--bslib-color-fg: #fff;--bslib-color-bg: #b2492c;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #b2492c;color:#fff}.bg-gradient-red-teal{--bslib-color-fg: #fff;--bslib-color-bg: #a6505f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #a6505f;color:#fff}.bg-gradient-red-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #d6226d;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #d6226d;color:#fff}.bg-gradient-orange-blue{--bslib-color-fg: #fff;--bslib-color-bg: #a09b8a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #a09b8a;color:#fff}.bg-gradient-orange-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #b96e90;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #b96e90;color:#fff}.bg-gradient-orange-purple{--bslib-color-fg: #fff;--bslib-color-bg: #b78060;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #b78060;color:#fff}.bg-gradient-orange-pink{--bslib-color-fg: #fff;--bslib-color-bg: #ed8167;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #ed8167;color:#fff}.bg-gradient-orange-red{--bslib-color-fg: #fff;--bslib-color-bg: #f66846;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #f66846;color:#fff}.bg-gradient-orange-yellow{--bslib-color-fg: #000;--bslib-color-bg: #f69738;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #f69738;color:#000}.bg-gradient-orange-green{--bslib-color-fg: #000;--bslib-color-bg: #a9b138;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #a9b138;color:#000}.bg-gradient-orange-teal{--bslib-color-fg: #000;--bslib-color-bg: #9db86b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #9db86b;color:#000}.bg-gradient-orange-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #cd897a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #cd897a;color:#fff}.bg-gradient-yellow-blue{--bslib-color-fg: #fff;--bslib-color-bg: #a97969;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #a97969;color:#fff}.bg-gradient-yellow-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #c24d6f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #c24d6f;color:#fff}.bg-gradient-yellow-purple{--bslib-color-fg: #fff;--bslib-color-bg: #c05f40;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #c05f40;color:#fff}.bg-gradient-yellow-pink{--bslib-color-fg: #fff;--bslib-color-bg: #f65f46;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #f65f46;color:#fff}.bg-gradient-yellow-red{--bslib-color-fg: #fff;--bslib-color-bg: #ff4625;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #ff4625;color:#fff}.bg-gradient-yellow-orange{--bslib-color-fg: #000;--bslib-color-bg: #f98b2e;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #f98b2e;color:#000}.bg-gradient-yellow-green{--bslib-color-fg: #fff;--bslib-color-bg: #b28f18;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #b28f18;color:#fff}.bg-gradient-yellow-teal{--bslib-color-fg: #fff;--bslib-color-bg: #a6974b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #a6974b;color:#fff}.bg-gradient-yellow-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #d66859;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #d66859;color:#fff}.bg-gradient-green-blue{--bslib-color-fg: #fff;--bslib-color-bg: #35a069;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #35a069;color:#fff}.bg-gradient-green-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #4f746f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #4f746f;color:#fff}.bg-gradient-green-purple{--bslib-color-fg: #fff;--bslib-color-bg: #4d8640;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #4d8640;color:#fff}.bg-gradient-green-pink{--bslib-color-fg: #fff;--bslib-color-bg: #838646;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #838646;color:#fff}.bg-gradient-green-red{--bslib-color-fg: #fff;--bslib-color-bg: #8c6d25;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #8c6d25;color:#fff}.bg-gradient-green-orange{--bslib-color-fg: #000;--bslib-color-bg: #86b22e;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #86b22e;color:#000}.bg-gradient-green-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #8c9c18;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #8c9c18;color:#fff}.bg-gradient-green-teal{--bslib-color-fg: #000;--bslib-color-bg: #33be4b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #33be4b;color:#000}.bg-gradient-green-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #638f59;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #638f59;color:#fff}.bg-gradient-teal-blue{--bslib-color-fg: #fff;--bslib-color-bg: #23acb5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #23acb5;color:#fff}.bg-gradient-teal-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #3c7fbb;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #3c7fbb;color:#fff}.bg-gradient-teal-purple{--bslib-color-fg: #fff;--bslib-color-bg: #3a918c;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #3a918c;color:#fff}.bg-gradient-teal-pink{--bslib-color-fg: #fff;--bslib-color-bg: #709193;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #709193;color:#fff}.bg-gradient-teal-red{--bslib-color-fg: #fff;--bslib-color-bg: #797971;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #797971;color:#fff}.bg-gradient-teal-orange{--bslib-color-fg: #000;--bslib-color-bg: #73be7a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #73be7a;color:#000}.bg-gradient-teal-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #79a764;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #79a764;color:#fff}.bg-gradient-teal-green{--bslib-color-fg: #000;--bslib-color-bg: #2cc164;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #2cc164;color:#000}.bg-gradient-teal-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #509aa5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #509aa5;color:#fff}.bg-gradient-cyan-blue{--bslib-color-fg: #fff;--bslib-color-bg: #6b66cb;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #6b66cb;color:#fff}.bg-gradient-cyan-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #8539d1;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #8539d1;color:#fff}.bg-gradient-cyan-purple{--bslib-color-fg: #fff;--bslib-color-bg: #834ba2;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #834ba2;color:#fff}.bg-gradient-cyan-pink{--bslib-color-fg: #fff;--bslib-color-bg: #b94ba8;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #b94ba8;color:#fff}.bg-gradient-cyan-red{--bslib-color-fg: #fff;--bslib-color-bg: #c23287;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #c23287;color:#fff}.bg-gradient-cyan-orange{--bslib-color-fg: #fff;--bslib-color-bg: #bc788f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #bc788f;color:#fff}.bg-gradient-cyan-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #c2617a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #c2617a;color:#fff}.bg-gradient-cyan-green{--bslib-color-fg: #fff;--bslib-color-bg: #757b7a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #757b7a;color:#fff}.bg-gradient-cyan-teal{--bslib-color-fg: #fff;--bslib-color-bg: #6983ad;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #6983ad;color:#fff}:root{--bslib-spacer: 1rem;--bslib-mb-spacer: var(--bslib-spacer, 1rem)}.bslib-mb-spacing{margin-bottom:var(--bslib-mb-spacer)}.bslib-gap-spacing{gap:var(--bslib-mb-spacer)}.bslib-gap-spacing>.bslib-mb-spacing,.bslib-gap-spacing>.form-group,.bslib-gap-spacing>p,.bslib-gap-spacing>pre{margin-bottom:0}.html-fill-container>.html-fill-item.bslib-mb-spacing{margin-bottom:0}.tab-content>.tab-pane.html-fill-container{display:none}.tab-content>.active.html-fill-container{display:flex}.tab-content.html-fill-container{padding:0}html{height:100%}.bslib-page-fill{width:100%;height:100%;margin:0;padding:var(--bslib-spacer, 1rem);gap:var(--bslib-spacer, 1rem)}@media(max-width: 575.98px){.bslib-page-fill{height:var(--bslib-page-fill-mobile-height, auto)}}.bslib-grid{display:grid !important;gap:var(--bslib-spacer, 1rem);height:var(--bslib-grid-height)}.bslib-grid.grid{grid-template-columns:repeat(var(--bs-columns, 12), minmax(0, 1fr));grid-template-rows:unset;grid-auto-rows:var(--bslib-grid--row-heights);--bslib-grid--row-heights--xs: unset;--bslib-grid--row-heights--sm: unset;--bslib-grid--row-heights--md: unset;--bslib-grid--row-heights--lg: unset;--bslib-grid--row-heights--xl: unset;--bslib-grid--row-heights--xxl: unset}.bslib-grid.grid.bslib-grid--row-heights--xs{--bslib-grid--row-heights: var(--bslib-grid--row-heights--xs)}@media(min-width: 576px){.bslib-grid.grid.bslib-grid--row-heights--sm{--bslib-grid--row-heights: var(--bslib-grid--row-heights--sm)}}@media(min-width: 768px){.bslib-grid.grid.bslib-grid--row-heights--md{--bslib-grid--row-heights: var(--bslib-grid--row-heights--md)}}@media(min-width: 992px){.bslib-grid.grid.bslib-grid--row-heights--lg{--bslib-grid--row-heights: var(--bslib-grid--row-heights--lg)}}@media(min-width: 1200px){.bslib-grid.grid.bslib-grid--row-heights--xl{--bslib-grid--row-heights: var(--bslib-grid--row-heights--xl)}}@media(min-width: 1400px){.bslib-grid.grid.bslib-grid--row-heights--xxl{--bslib-grid--row-heights: var(--bslib-grid--row-heights--xxl)}}.bslib-grid>*>.shiny-input-container{width:100%}.bslib-grid-item{grid-column:auto/span 1}@media(max-width: 767.98px){.bslib-grid-item{grid-column:1/-1}}@media(max-width: 575.98px){.bslib-grid{grid-template-columns:1fr !important;height:var(--bslib-grid-height-mobile)}.bslib-grid.grid{height:unset !important;grid-auto-rows:var(--bslib-grid--row-heights--xs, auto)}}.navbar+.container-fluid:has(>.tab-content>.tab-pane.active.html-fill-container),.navbar+.container-sm:has(>.tab-content>.tab-pane.active.html-fill-container),.navbar+.container-md:has(>.tab-content>.tab-pane.active.html-fill-container),.navbar+.container-lg:has(>.tab-content>.tab-pane.active.html-fill-container),.navbar+.container-xl:has(>.tab-content>.tab-pane.active.html-fill-container),.navbar+.container-xxl:has(>.tab-content>.tab-pane.active.html-fill-container){padding-left:0;padding-right:0}.navbar+.container-fluid>.tab-content>.tab-pane.active.html-fill-container,.navbar+.container-sm>.tab-content>.tab-pane.active.html-fill-container,.navbar+.container-md>.tab-content>.tab-pane.active.html-fill-container,.navbar+.container-lg>.tab-content>.tab-pane.active.html-fill-container,.navbar+.container-xl>.tab-content>.tab-pane.active.html-fill-container,.navbar+.container-xxl>.tab-content>.tab-pane.active.html-fill-container{padding:var(--bslib-spacer, 1rem);gap:var(--bslib-spacer, 1rem)}.navbar+.container-fluid>.tab-content>.tab-pane.active.html-fill-container:has(>.bslib-sidebar-layout:only-child),.navbar+.container-sm>.tab-content>.tab-pane.active.html-fill-container:has(>.bslib-sidebar-layout:only-child),.navbar+.container-md>.tab-content>.tab-pane.active.html-fill-container:has(>.bslib-sidebar-layout:only-child),.navbar+.container-lg>.tab-content>.tab-pane.active.html-fill-container:has(>.bslib-sidebar-layout:only-child),.navbar+.container-xl>.tab-content>.tab-pane.active.html-fill-container:has(>.bslib-sidebar-layout:only-child),.navbar+.container-xxl>.tab-content>.tab-pane.active.html-fill-container:has(>.bslib-sidebar-layout:only-child){padding:0}.navbar+.container-fluid>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border=true]),.navbar+.container-sm>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border=true]),.navbar+.container-md>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border=true]),.navbar+.container-lg>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border=true]),.navbar+.container-xl>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border=true]),.navbar+.container-xxl>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border=true]){border-left:none;border-right:none;border-bottom:none}.navbar+.container-fluid>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border-radius=true]),.navbar+.container-sm>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border-radius=true]),.navbar+.container-md>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border-radius=true]),.navbar+.container-lg>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border-radius=true]),.navbar+.container-xl>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border-radius=true]),.navbar+.container-xxl>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border-radius=true]){border-radius:0}.navbar+div>.bslib-sidebar-layout{border-top:var(--bslib-sidebar-border)}.bslib-card{overflow:auto}.bslib-card .card-body+.card-body{padding-top:0}.bslib-card .card-body{overflow:auto}.bslib-card .card-body p{margin-top:0}.bslib-card .card-body p:last-child{margin-bottom:0}.bslib-card .card-body{max-height:var(--bslib-card-body-max-height, none)}.bslib-card[data-full-screen=true]>.card-body{max-height:var(--bslib-card-body-max-height-full-screen, none)}.bslib-card .card-header .form-group{margin-bottom:0}.bslib-card .card-header .selectize-control{margin-bottom:0}.bslib-card .card-header .selectize-control .item{margin-right:1.15rem}.bslib-card .card-footer{margin-top:auto}.bslib-card .bslib-navs-card-title{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center}.bslib-card .bslib-navs-card-title .nav{margin-left:auto}.bslib-card .bslib-sidebar-layout:not([data-bslib-sidebar-border=true]){border:none}.bslib-card .bslib-sidebar-layout:not([data-bslib-sidebar-border-radius=true]){border-top-left-radius:0;border-top-right-radius:0}[data-full-screen=true]{position:fixed;inset:3.5rem 1rem 1rem;height:auto !important;max-height:none !important;width:auto !important;z-index:1070}.bslib-full-screen-enter{display:none;position:absolute;bottom:var(--bslib-full-screen-enter-bottom, 0.2rem);right:var(--bslib-full-screen-enter-right, 0);top:var(--bslib-full-screen-enter-top);left:var(--bslib-full-screen-enter-left);color:var(--bslib-color-fg, var(--bs-card-color));background-color:var(--bslib-color-bg, var(--bs-card-bg, var(--bs-body-bg)));border:var(--bs-card-border-width) solid var(--bslib-color-fg, var(--bs-card-border-color));box-shadow:0 2px 4px rgba(0,0,0,.15);margin:.2rem .4rem;padding:.55rem !important;font-size:.8rem;cursor:pointer;opacity:.7;z-index:1070}.bslib-full-screen-enter:hover{opacity:1}.card[data-full-screen=false]:hover>*>.bslib-full-screen-enter{display:block}.bslib-has-full-screen .card:hover>*>.bslib-full-screen-enter{display:none}@media(max-width: 575.98px){.bslib-full-screen-enter{display:none !important}}.bslib-full-screen-exit{position:relative;top:1.35rem;font-size:.9rem;cursor:pointer;text-decoration:none;display:flex;float:right;margin-right:2.15rem;align-items:center;color:rgba(var(--bs-body-bg-rgb), 0.8)}.bslib-full-screen-exit:hover{color:rgba(var(--bs-body-bg-rgb), 1)}.bslib-full-screen-exit svg{margin-left:.5rem;font-size:1.5rem}#bslib-full-screen-overlay{position:fixed;inset:0;background-color:rgba(var(--bs-body-color-rgb), 0.6);backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);z-index:1069;animation:bslib-full-screen-overlay-enter 400ms cubic-bezier(0.6, 0.02, 0.65, 1) forwards}@keyframes bslib-full-screen-overlay-enter{0%{opacity:0}100%{opacity:1}}.accordion .accordion-header{font-size:calc(1.29rem + 0.48vw);margin-top:0;margin-bottom:.5rem;font-weight:400;line-height:1.2;color:var(--bs-heading-color);margin-bottom:0}@media(min-width: 1200px){.accordion .accordion-header{font-size:1.65rem}}.accordion .accordion-icon:not(:empty){margin-right:.75rem;display:flex}.accordion .accordion-button:not(.collapsed){box-shadow:none}.accordion .accordion-button:not(.collapsed):focus{box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.bslib-sidebar-layout{--bslib-sidebar-transition-duration: 500ms;--bslib-sidebar-transition-easing-x: cubic-bezier(0.8, 0.78, 0.22, 1.07);--bslib-sidebar-border: var(--bs-card-border-width, 1px) solid var(--bs-card-border-color, rgba(0, 0, 0, 0.175));--bslib-sidebar-border-radius: var(--bs-border-radius);--bslib-sidebar-vert-border: var(--bs-card-border-width, 1px) solid var(--bs-card-border-color, rgba(0, 0, 0, 0.175));--bslib-sidebar-bg: rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.05);--bslib-sidebar-fg: var(--bs-emphasis-color, black);--bslib-sidebar-main-fg: var(--bs-card-color, var(--bs-body-color));--bslib-sidebar-main-bg: var(--bs-card-bg, var(--bs-body-bg));--bslib-sidebar-toggle-bg: rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.1);--bslib-sidebar-padding: calc(var(--bslib-spacer) * 1.5);--bslib-sidebar-icon-size: var(--bslib-spacer, 1rem);--bslib-sidebar-icon-button-size: calc(var(--bslib-sidebar-icon-size, 1rem) * 2);--bslib-sidebar-padding-icon: calc(var(--bslib-sidebar-icon-button-size, 2rem) * 1.5);--bslib-collapse-toggle-border-radius: var(--bs-border-radius, 0.25rem);--bslib-collapse-toggle-transform: 0deg;--bslib-sidebar-toggle-transition-easing: cubic-bezier(1, 0, 0, 1);--bslib-collapse-toggle-right-transform: 180deg;--bslib-sidebar-column-main: minmax(0, 1fr);display:grid !important;grid-template-columns:min(100% - var(--bslib-sidebar-icon-size),var(--bslib-sidebar-width, 250px)) var(--bslib-sidebar-column-main);position:relative;transition:grid-template-columns ease-in-out var(--bslib-sidebar-transition-duration);border:var(--bslib-sidebar-border);border-radius:var(--bslib-sidebar-border-radius)}@media(prefers-reduced-motion: reduce){.bslib-sidebar-layout{transition:none}}.bslib-sidebar-layout[data-bslib-sidebar-border=false]{border:none}.bslib-sidebar-layout[data-bslib-sidebar-border-radius=false]{border-radius:initial}.bslib-sidebar-layout>.main,.bslib-sidebar-layout>.sidebar{grid-row:1/2;border-radius:inherit;overflow:auto}.bslib-sidebar-layout>.main{grid-column:2/3;border-top-left-radius:0;border-bottom-left-radius:0;padding:var(--bslib-sidebar-padding);transition:padding var(--bslib-sidebar-transition-easing-x) var(--bslib-sidebar-transition-duration);color:var(--bslib-sidebar-main-fg);background-color:var(--bslib-sidebar-main-bg)}.bslib-sidebar-layout>.sidebar{grid-column:1/2;width:100%;height:100%;border-right:var(--bslib-sidebar-vert-border);border-top-right-radius:0;border-bottom-right-radius:0;color:var(--bslib-sidebar-fg);background-color:var(--bslib-sidebar-bg);backdrop-filter:blur(5px)}.bslib-sidebar-layout>.sidebar>.sidebar-content{display:flex;flex-direction:column;gap:var(--bslib-spacer, 1rem);padding:var(--bslib-sidebar-padding);padding-top:var(--bslib-sidebar-padding-icon)}.bslib-sidebar-layout>.sidebar>.sidebar-content>:last-child:not(.sidebar-title){margin-bottom:0}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion{margin-left:calc(-1*var(--bslib-sidebar-padding));margin-right:calc(-1*var(--bslib-sidebar-padding))}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion:last-child{margin-bottom:calc(-1*var(--bslib-sidebar-padding))}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion:not(:last-child){margin-bottom:1rem}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion .accordion-body{display:flex;flex-direction:column}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion:not(:first-child) .accordion-item:first-child{border-top:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion:not(:last-child) .accordion-item:last-child{border-bottom:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.bslib-sidebar-layout>.sidebar>.sidebar-content.has-accordion>.sidebar-title{border-bottom:none;padding-bottom:0}.bslib-sidebar-layout>.sidebar .shiny-input-container{width:100%}.bslib-sidebar-layout[data-bslib-sidebar-open=always]>.sidebar>.sidebar-content{padding-top:var(--bslib-sidebar-padding)}.bslib-sidebar-layout>.collapse-toggle{grid-row:1/2;grid-column:1/2;display:inline-flex;align-items:center;position:absolute;right:calc(var(--bslib-sidebar-icon-size));top:calc(var(--bslib-sidebar-icon-size, 1rem)/2);border:none;border-radius:var(--bslib-collapse-toggle-border-radius);height:var(--bslib-sidebar-icon-button-size, 2rem);width:var(--bslib-sidebar-icon-button-size, 2rem);display:flex;align-items:center;justify-content:center;padding:0;color:var(--bslib-sidebar-fg);background-color:unset;transition:color var(--bslib-sidebar-transition-easing-x) var(--bslib-sidebar-transition-duration),top var(--bslib-sidebar-transition-easing-x) var(--bslib-sidebar-transition-duration),right var(--bslib-sidebar-transition-easing-x) var(--bslib-sidebar-transition-duration),left var(--bslib-sidebar-transition-easing-x) var(--bslib-sidebar-transition-duration)}.bslib-sidebar-layout>.collapse-toggle:hover{background-color:var(--bslib-sidebar-toggle-bg)}.bslib-sidebar-layout>.collapse-toggle>.collapse-icon{opacity:.8;width:var(--bslib-sidebar-icon-size);height:var(--bslib-sidebar-icon-size);transform:rotateY(var(--bslib-collapse-toggle-transform));transition:transform var(--bslib-sidebar-toggle-transition-easing) var(--bslib-sidebar-transition-duration)}.bslib-sidebar-layout>.collapse-toggle:hover>.collapse-icon{opacity:1}.bslib-sidebar-layout .sidebar-title{font-size:1.25rem;line-height:1.25;margin-top:0;margin-bottom:1rem;padding-bottom:1rem;border-bottom:var(--bslib-sidebar-border)}.bslib-sidebar-layout.sidebar-right{grid-template-columns:var(--bslib-sidebar-column-main) min(100% - var(--bslib-sidebar-icon-size),var(--bslib-sidebar-width, 250px))}.bslib-sidebar-layout.sidebar-right>.main{grid-column:1/2;border-top-right-radius:0;border-bottom-right-radius:0;border-top-left-radius:inherit;border-bottom-left-radius:inherit}.bslib-sidebar-layout.sidebar-right>.sidebar{grid-column:2/3;border-right:none;border-left:var(--bslib-sidebar-vert-border);border-top-left-radius:0;border-bottom-left-radius:0}.bslib-sidebar-layout.sidebar-right>.collapse-toggle{grid-column:2/3;left:var(--bslib-sidebar-icon-size);right:unset;border:var(--bslib-collapse-toggle-border)}.bslib-sidebar-layout.sidebar-right>.collapse-toggle>.collapse-icon{transform:rotateY(var(--bslib-collapse-toggle-right-transform))}.bslib-sidebar-layout.sidebar-collapsed{--bslib-collapse-toggle-transform: 180deg;--bslib-collapse-toggle-right-transform: 0deg;--bslib-sidebar-vert-border: none;grid-template-columns:0 minmax(0, 1fr)}.bslib-sidebar-layout.sidebar-collapsed.sidebar-right{grid-template-columns:minmax(0, 1fr) 0}.bslib-sidebar-layout.sidebar-collapsed:not(.transitioning)>.sidebar>*{display:none}.bslib-sidebar-layout.sidebar-collapsed>.main{border-radius:inherit}.bslib-sidebar-layout.sidebar-collapsed:not(.sidebar-right)>.main{padding-left:var(--bslib-sidebar-padding-icon)}.bslib-sidebar-layout.sidebar-collapsed.sidebar-right>.main{padding-right:var(--bslib-sidebar-padding-icon)}.bslib-sidebar-layout.sidebar-collapsed>.collapse-toggle{color:var(--bslib-sidebar-main-fg);top:calc(var(--bslib-sidebar-overlap-counter, 0)*(var(--bslib-sidebar-icon-size) + var(--bslib-sidebar-padding)) + var(--bslib-sidebar-icon-size, 1rem)/2);right:calc(-2.5*var(--bslib-sidebar-icon-size) - var(--bs-card-border-width, 1px))}.bslib-sidebar-layout.sidebar-collapsed.sidebar-right>.collapse-toggle{left:calc(-2.5*var(--bslib-sidebar-icon-size) - var(--bs-card-border-width, 1px));right:unset}@media(min-width: 576px){.bslib-sidebar-layout.transitioning>.sidebar>.sidebar-content{display:none}}@media(max-width: 575.98px){.bslib-sidebar-layout[data-bslib-sidebar-open=desktop]{--bslib-sidebar-js-init-collapsed: true}.bslib-sidebar-layout>.sidebar,.bslib-sidebar-layout.sidebar-right>.sidebar{border:none}.bslib-sidebar-layout>.main,.bslib-sidebar-layout.sidebar-right>.main{grid-column:1/3}.bslib-sidebar-layout[data-bslib-sidebar-open=always]{display:block !important}.bslib-sidebar-layout[data-bslib-sidebar-open=always]>.sidebar{max-height:var(--bslib-sidebar-max-height-mobile);overflow-y:auto;border-top:var(--bslib-sidebar-vert-border)}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]){grid-template-columns:100% 0}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]):not(.sidebar-collapsed)>.sidebar{z-index:1}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]):not(.sidebar-collapsed)>.collapse-toggle{z-index:1}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]).sidebar-right{grid-template-columns:0 100%}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]).sidebar-collapsed{grid-template-columns:0 100%}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]).sidebar-collapsed.sidebar-right{grid-template-columns:100% 0}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]):not(.sidebar-right)>.main{padding-left:var(--bslib-sidebar-padding-icon)}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]).sidebar-right>.main{padding-right:var(--bslib-sidebar-padding-icon)}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always])>.main{opacity:0;transition:opacity var(--bslib-sidebar-transition-easing-x) var(--bslib-sidebar-transition-duration)}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]).sidebar-collapsed>.main{opacity:1}}:root{--bslib-value-box-shadow: none;--bslib-value-box-border-width-auto-yes: var(--bslib-value-box-border-width-baseline);--bslib-value-box-border-width-auto-no: 0;--bslib-value-box-border-width-baseline: 1px}.bslib-value-box{border-width:var(--bslib-value-box-border-width-auto-no, var(--bslib-value-box-border-width-baseline));container-name:bslib-value-box;container-type:inline-size}.bslib-value-box.card{box-shadow:var(--bslib-value-box-shadow)}.bslib-value-box.border-auto{border-width:var(--bslib-value-box-border-width-auto-yes, var(--bslib-value-box-border-width-baseline))}.bslib-value-box.default{--bslib-value-box-bg-default: var(--bs-card-bg, #FFFFFF);--bslib-value-box-border-color-default: var(--bs-card-border-color, rgba(0, 0, 0, 0.175));color:var(--bslib-value-box-color);background-color:var(--bslib-value-box-bg, var(--bslib-value-box-bg-default));border-color:var(--bslib-value-box-border-color, var(--bslib-value-box-border-color-default))}.bslib-value-box .value-box-grid{display:grid;grid-template-areas:"left right";align-items:center;overflow:hidden}.bslib-value-box .value-box-showcase{height:100%;max-height:var(---bslib-value-box-showcase-max-h, 100%)}.bslib-value-box .value-box-showcase,.bslib-value-box .value-box-showcase>.html-fill-item{width:100%}.bslib-value-box[data-full-screen=true] .value-box-showcase{max-height:var(---bslib-value-box-showcase-max-h-fs, 100%)}@media screen and (min-width: 575.98px){@container bslib-value-box (max-width: 300px){.bslib-value-box:not(.showcase-bottom) .value-box-grid{grid-template-columns:1fr !important;grid-template-rows:auto auto;grid-template-areas:"top" "bottom"}.bslib-value-box:not(.showcase-bottom) .value-box-grid .value-box-showcase{grid-area:top !important}.bslib-value-box:not(.showcase-bottom) .value-box-grid .value-box-area{grid-area:bottom !important;justify-content:end}}}.bslib-value-box .value-box-area{justify-content:center;padding:1.5rem 1rem;font-size:.9rem;font-weight:500}.bslib-value-box .value-box-area *{margin-bottom:0;margin-top:0}.bslib-value-box .value-box-title{font-size:1rem;margin-top:0;margin-bottom:.5rem;font-weight:400;line-height:1.2}.bslib-value-box .value-box-title:empty::after{content:" "}.bslib-value-box .value-box-value{font-size:calc(1.29rem + 0.48vw);margin-top:0;margin-bottom:.5rem;font-weight:400;line-height:1.2}@media(min-width: 1200px){.bslib-value-box .value-box-value{font-size:1.65rem}}.bslib-value-box .value-box-value:empty::after{content:" "}.bslib-value-box .value-box-showcase{align-items:center;justify-content:center;margin-top:auto;margin-bottom:auto;padding:1rem}.bslib-value-box .value-box-showcase .bi,.bslib-value-box .value-box-showcase .fa,.bslib-value-box .value-box-showcase .fab,.bslib-value-box .value-box-showcase .fas,.bslib-value-box .value-box-showcase .far{opacity:.85;min-width:50px;max-width:125%}.bslib-value-box .value-box-showcase .bi,.bslib-value-box .value-box-showcase .fa,.bslib-value-box .value-box-showcase .fab,.bslib-value-box .value-box-showcase .fas,.bslib-value-box .value-box-showcase .far{font-size:4rem}.bslib-value-box.showcase-top-right .value-box-grid{grid-template-columns:1fr var(---bslib-value-box-showcase-w, 50%)}.bslib-value-box.showcase-top-right .value-box-grid .value-box-showcase{grid-area:right;margin-left:auto;align-self:start;align-items:end;padding-left:0;padding-bottom:0}.bslib-value-box.showcase-top-right .value-box-grid .value-box-area{grid-area:left;align-self:end}.bslib-value-box.showcase-top-right[data-full-screen=true] .value-box-grid{grid-template-columns:auto var(---bslib-value-box-showcase-w-fs, 1fr)}.bslib-value-box.showcase-top-right[data-full-screen=true] .value-box-grid>div{align-self:center}.bslib-value-box.showcase-top-right:not([data-full-screen=true]) .value-box-showcase{margin-top:0}@container bslib-value-box (max-width: 300px){.bslib-value-box.showcase-top-right:not([data-full-screen=true]) .value-box-grid .value-box-showcase{padding-left:1rem}}.bslib-value-box.showcase-left-center .value-box-grid{grid-template-columns:var(---bslib-value-box-showcase-w, 30%) auto}.bslib-value-box.showcase-left-center[data-full-screen=true] .value-box-grid{grid-template-columns:var(---bslib-value-box-showcase-w-fs, 1fr) auto}.bslib-value-box.showcase-left-center:not([data-fill-screen=true]) .value-box-grid .value-box-showcase{grid-area:left}.bslib-value-box.showcase-left-center:not([data-fill-screen=true]) .value-box-grid .value-box-area{grid-area:right}.bslib-value-box.showcase-bottom .value-box-grid{grid-template-columns:1fr;grid-template-rows:1fr var(---bslib-value-box-showcase-h, auto);grid-template-areas:"top" "bottom";overflow:hidden}.bslib-value-box.showcase-bottom .value-box-grid .value-box-showcase{grid-area:bottom;padding:0;margin:0}.bslib-value-box.showcase-bottom .value-box-grid .value-box-area{grid-area:top}.bslib-value-box.showcase-bottom[data-full-screen=true] .value-box-grid{grid-template-rows:1fr var(---bslib-value-box-showcase-h-fs, 2fr)}.bslib-value-box.showcase-bottom[data-full-screen=true] .value-box-grid .value-box-showcase{padding:1rem}[data-bs-theme=dark] .bslib-value-box{--bslib-value-box-shadow: 0 0.5rem 1rem rgb(0 0 0 / 50%)}:root{--bslib-page-sidebar-title-bg: #3CDD8C;--bslib-page-sidebar-title-color: #000}.bslib-page-title{background-color:var(--bslib-page-sidebar-title-bg);color:var(--bslib-page-sidebar-title-color);font-size:1.25rem;font-weight:300;padding:var(--bslib-spacer, 1rem);padding-left:1.5rem;margin-bottom:0;border-bottom:1px solid #dee2e6}@media(min-width: 576px){.nav:not(.nav-hidden){display:flex !important;display:-webkit-flex !important}.nav:not(.nav-hidden):not(.nav-stacked):not(.flex-column){float:none !important}.nav:not(.nav-hidden):not(.nav-stacked):not(.flex-column)>.bslib-nav-spacer{margin-left:auto !important}.nav:not(.nav-hidden):not(.nav-stacked):not(.flex-column)>.form-inline{margin-top:auto;margin-bottom:auto}.nav:not(.nav-hidden).nav-stacked{flex-direction:column;-webkit-flex-direction:column;height:100%}.nav:not(.nav-hidden).nav-stacked>.bslib-nav-spacer{margin-top:auto !important}}.html-fill-container{display:flex;flex-direction:column;min-height:0;min-width:0}.html-fill-container>.html-fill-item{flex:1 1 auto;min-height:0;min-width:0}.html-fill-container>:not(.html-fill-item){flex:0 0 auto}.quarto-container{min-height:calc(100vh - 132px)}body.hypothesis-enabled #quarto-header{margin-right:16px}footer.footer .nav-footer,#quarto-header>nav{padding-left:1em;padding-right:1em}footer.footer div.nav-footer p:first-child{margin-top:0}footer.footer div.nav-footer p:last-child{margin-bottom:0}#quarto-content>*{padding-top:14px}#quarto-content>#quarto-sidebar-glass{padding-top:0px}@media(max-width: 991.98px){#quarto-content>*{padding-top:0}#quarto-content .subtitle{padding-top:14px}#quarto-content section:first-of-type h2:first-of-type,#quarto-content section:first-of-type .h2:first-of-type{margin-top:1rem}}.headroom-target,header.headroom{will-change:transform;transition:position 200ms linear;transition:all 200ms linear}header.headroom--pinned{transform:translateY(0%)}header.headroom--unpinned{transform:translateY(-100%)}.navbar-container{width:100%}.navbar-brand{overflow:hidden;text-overflow:ellipsis}.navbar-brand-container{max-width:calc(100% - 115px);min-width:0;display:flex;align-items:center}@media(min-width: 992px){.navbar-brand-container{margin-right:1em}}.navbar-brand.navbar-brand-logo{margin-right:4px;display:inline-flex}.navbar-toggler{flex-basis:content;flex-shrink:0}.navbar .navbar-brand-container{order:2}.navbar .navbar-toggler{order:1}.navbar .navbar-container>.navbar-nav{order:20}.navbar .navbar-container>.navbar-brand-container{margin-left:0 !important;margin-right:0 !important}.navbar .navbar-collapse{order:20}.navbar #quarto-search{order:4;margin-left:auto}.navbar .navbar-toggler{margin-right:.5em}.navbar-collapse .quarto-navbar-tools{margin-left:.5em}.navbar-logo{max-height:24px;width:auto;padding-right:4px}nav .nav-item:not(.compact){padding-top:1px}nav .nav-link i,nav .dropdown-item i{padding-right:1px}.navbar-expand-lg .navbar-nav .nav-link{padding-left:.6rem;padding-right:.6rem}nav .nav-item.compact .nav-link{padding-left:.5rem;padding-right:.5rem;font-size:1.1rem}.navbar .quarto-navbar-tools{order:3}.navbar .quarto-navbar-tools div.dropdown{display:inline-block}.navbar .quarto-navbar-tools .quarto-navigation-tool{color:#fff}.navbar .quarto-navbar-tools .quarto-navigation-tool:hover{color:#000}.navbar-nav .dropdown-menu{min-width:220px;font-size:.9rem}.navbar .navbar-nav .nav-link.dropdown-toggle::after{opacity:.75;vertical-align:.175em}.navbar ul.dropdown-menu{padding-top:0;padding-bottom:0}.navbar .dropdown-header{text-transform:uppercase;font-size:.8rem;padding:0 .5rem}.navbar .dropdown-item{padding:.4rem .5rem}.navbar .dropdown-item>i.bi{margin-left:.1rem;margin-right:.25em}.sidebar #quarto-search{margin-top:-1px}.sidebar #quarto-search svg.aa-SubmitIcon{width:16px;height:16px}.sidebar-navigation a{color:inherit}.sidebar-title{margin-top:.25rem;padding-bottom:.5rem;font-size:1.3rem;line-height:1.6rem;visibility:visible}.sidebar-title>a{font-size:inherit;text-decoration:none}.sidebar-title .sidebar-tools-main{margin-top:-6px}@media(max-width: 991.98px){#quarto-sidebar div.sidebar-header{padding-top:.2em}}.sidebar-header-stacked .sidebar-title{margin-top:.6rem}.sidebar-logo{max-width:90%;padding-bottom:.5rem}.sidebar-logo-link{text-decoration:none}.sidebar-navigation li a{text-decoration:none}.sidebar-navigation .quarto-navigation-tool{opacity:.7;font-size:.875rem}#quarto-sidebar>nav>.sidebar-tools-main{margin-left:14px}.sidebar-tools-main{display:inline-flex;margin-left:0px;order:2}.sidebar-tools-main:not(.tools-wide){vertical-align:middle}.sidebar-navigation .quarto-navigation-tool.dropdown-toggle::after{display:none}.sidebar.sidebar-navigation>*{padding-top:1em}.sidebar-item{margin-bottom:.2em;line-height:1rem;margin-top:.4rem}.sidebar-section{padding-left:.5em;padding-bottom:.2em}.sidebar-item .sidebar-item-container{display:flex;justify-content:space-between;cursor:pointer}.sidebar-item-toggle:hover{cursor:pointer}.sidebar-item .sidebar-item-toggle .bi{font-size:.7rem;text-align:center}.sidebar-item .sidebar-item-toggle .bi-chevron-right::before{transition:transform 200ms ease}.sidebar-item .sidebar-item-toggle[aria-expanded=false] .bi-chevron-right::before{transform:none}.sidebar-item .sidebar-item-toggle[aria-expanded=true] .bi-chevron-right::before{transform:rotate(90deg)}.sidebar-item-text{width:100%}.sidebar-navigation .sidebar-divider{margin-left:0;margin-right:0;margin-top:.5rem;margin-bottom:.5rem}@media(max-width: 991.98px){.quarto-secondary-nav{display:block}.quarto-secondary-nav button.quarto-search-button{padding-right:0em;padding-left:2em}.quarto-secondary-nav button.quarto-btn-toggle{margin-left:-0.75rem;margin-right:.15rem}.quarto-secondary-nav nav.quarto-title-breadcrumbs{display:none}.quarto-secondary-nav nav.quarto-page-breadcrumbs{display:flex;align-items:center;padding-right:1em;margin-left:-0.25em}.quarto-secondary-nav nav.quarto-page-breadcrumbs a{text-decoration:none}.quarto-secondary-nav nav.quarto-page-breadcrumbs ol.breadcrumb{margin-bottom:0}}@media(min-width: 992px){.quarto-secondary-nav{display:none}}.quarto-title-breadcrumbs .breadcrumb{margin-bottom:.5em;font-size:.9rem}.quarto-title-breadcrumbs .breadcrumb li:last-of-type a{color:#6c757d}.quarto-secondary-nav .quarto-btn-toggle{color:#595959}.quarto-secondary-nav[aria-expanded=false] .quarto-btn-toggle .bi-chevron-right::before{transform:none}.quarto-secondary-nav[aria-expanded=true] .quarto-btn-toggle .bi-chevron-right::before{transform:rotate(90deg)}.quarto-secondary-nav .quarto-btn-toggle .bi-chevron-right::before{transition:transform 200ms ease}.quarto-secondary-nav{cursor:pointer}.no-decor{text-decoration:none}.quarto-secondary-nav-title{margin-top:.3em;color:#595959;padding-top:4px}.quarto-secondary-nav nav.quarto-page-breadcrumbs{color:#595959}.quarto-secondary-nav nav.quarto-page-breadcrumbs a{color:#595959}.quarto-secondary-nav nav.quarto-page-breadcrumbs a:hover{color:rgba(64,64,191,.8)}.quarto-secondary-nav nav.quarto-page-breadcrumbs .breadcrumb-item::before{color:#8c8c8c}.breadcrumb-item{line-height:1.2rem}div.sidebar-item-container{color:#595959}div.sidebar-item-container:hover,div.sidebar-item-container:focus{color:rgba(64,64,191,.8)}div.sidebar-item-container.disabled{color:rgba(89,89,89,.75)}div.sidebar-item-container .active,div.sidebar-item-container .show>.nav-link,div.sidebar-item-container .sidebar-link>code{color:#4040bf}div.sidebar.sidebar-navigation.rollup.quarto-sidebar-toggle-contents,nav.sidebar.sidebar-navigation:not(.rollup){background-color:#fff}@media(max-width: 991.98px){.sidebar-navigation .sidebar-item a,.nav-page .nav-page-text,.sidebar-navigation{font-size:1rem}.sidebar-navigation ul.sidebar-section.depth1 .sidebar-section-item{font-size:1.1rem}.sidebar-logo{display:none}.sidebar.sidebar-navigation{position:static;border-bottom:1px solid #dee2e6}.sidebar.sidebar-navigation.collapsing{position:fixed;z-index:1000}.sidebar.sidebar-navigation.show{position:fixed;z-index:1000}.sidebar.sidebar-navigation{min-height:100%}nav.quarto-secondary-nav{background-color:#fff;border-bottom:1px solid #dee2e6}.quarto-banner nav.quarto-secondary-nav{background-color:#3cdd8c;color:#fff;border-top:1px solid #dee2e6}.sidebar .sidebar-footer{visibility:visible;padding-top:1rem;position:inherit}.sidebar-tools-collapse{display:block}}#quarto-sidebar{transition:width .15s ease-in}#quarto-sidebar>*{padding-right:1em}@media(max-width: 991.98px){#quarto-sidebar .sidebar-menu-container{white-space:nowrap;min-width:225px}#quarto-sidebar.show{transition:width .15s ease-out}}@media(min-width: 992px){#quarto-sidebar{display:flex;flex-direction:column}.nav-page .nav-page-text,.sidebar-navigation .sidebar-section .sidebar-item{font-size:.875rem}.sidebar-navigation .sidebar-item{font-size:.925rem}.sidebar.sidebar-navigation{display:block;position:sticky}.sidebar-search{width:100%}.sidebar .sidebar-footer{visibility:visible}}@media(min-width: 992px){#quarto-sidebar-glass{display:none}}@media(max-width: 991.98px){#quarto-sidebar-glass{position:fixed;top:0;bottom:0;left:0;right:0;background-color:rgba(255,255,255,0);transition:background-color .15s ease-in;z-index:-1}#quarto-sidebar-glass.collapsing{z-index:1000}#quarto-sidebar-glass.show{transition:background-color .15s ease-out;background-color:rgba(102,102,102,.4);z-index:1000}}.sidebar .sidebar-footer{padding:.5rem 1rem;align-self:flex-end;color:#6c757d;width:100%}.quarto-page-breadcrumbs .breadcrumb-item+.breadcrumb-item,.quarto-page-breadcrumbs .breadcrumb-item{padding-right:.33em;padding-left:0}.quarto-page-breadcrumbs .breadcrumb-item::before{padding-right:.33em}.quarto-sidebar-footer{font-size:.875em}.sidebar-section .bi-chevron-right{vertical-align:middle}.sidebar-section .bi-chevron-right::before{font-size:.9em}.notransition{-webkit-transition:none !important;-moz-transition:none !important;-o-transition:none !important;transition:none !important}.btn:focus:not(:focus-visible){box-shadow:none}.page-navigation{display:flex;justify-content:space-between}.nav-page{padding-bottom:.75em}.nav-page .bi{font-size:1.8rem;vertical-align:middle}.nav-page .nav-page-text{padding-left:.25em;padding-right:.25em}.nav-page a{color:#6c757d;text-decoration:none;display:flex;align-items:center}.nav-page a:hover{color:#339}.nav-footer .toc-actions{padding-bottom:.5em;padding-top:.5em}.nav-footer .toc-actions a,.nav-footer .toc-actions a:hover{text-decoration:none}.nav-footer .toc-actions ul{display:flex;list-style:none}.nav-footer .toc-actions ul :first-child{margin-left:auto}.nav-footer .toc-actions ul :last-child{margin-right:auto}.nav-footer .toc-actions ul li{padding-right:1.5em}.nav-footer .toc-actions ul li i.bi{padding-right:.4em}.nav-footer .toc-actions ul li:last-of-type{padding-right:0}.nav-footer{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:baseline;text-align:center;padding-top:.5rem;padding-bottom:.5rem;background-color:#fff}body.nav-fixed{padding-top:64px}.nav-footer-contents{color:#6c757d;margin-top:.25rem}.nav-footer{min-height:3.5em;color:#757575}.nav-footer a{color:#757575}.nav-footer .nav-footer-left{font-size:.825em}.nav-footer .nav-footer-center{font-size:.825em}.nav-footer .nav-footer-right{font-size:.825em}.nav-footer-left .footer-items,.nav-footer-center .footer-items,.nav-footer-right .footer-items{display:inline-flex;padding-top:.3em;padding-bottom:.3em;margin-bottom:0em}.nav-footer-left .footer-items .nav-link,.nav-footer-center .footer-items .nav-link,.nav-footer-right .footer-items .nav-link{padding-left:.6em;padding-right:.6em}@media(min-width: 768px){.nav-footer-left{flex:1 1 0px;text-align:left}}@media(max-width: 575.98px){.nav-footer-left{margin-bottom:1em;flex:100%}}@media(min-width: 768px){.nav-footer-right{flex:1 1 0px;text-align:right}}@media(max-width: 575.98px){.nav-footer-right{margin-bottom:1em;flex:100%}}.nav-footer-center{text-align:center;min-height:3em}@media(min-width: 768px){.nav-footer-center{flex:1 1 0px}}.nav-footer-center .footer-items{justify-content:center}@media(max-width: 767.98px){.nav-footer-center{margin-bottom:1em;flex:100%}}@media(max-width: 767.98px){.nav-footer-center{margin-top:3em;order:10}}.navbar .quarto-reader-toggle.reader .quarto-reader-toggle-btn{background-color:#fff;border-radius:3px}@media(max-width: 991.98px){.quarto-reader-toggle{display:none}}.quarto-reader-toggle.reader.quarto-navigation-tool .quarto-reader-toggle-btn{background-color:#595959;border-radius:3px}.quarto-reader-toggle .quarto-reader-toggle-btn{display:inline-flex;padding-left:.2em;padding-right:.2em;margin-left:-0.2em;margin-right:-0.2em;text-align:center}.navbar .quarto-reader-toggle:not(.reader) .bi::before{background-image:url('data:image/svg+xml,')}.navbar .quarto-reader-toggle.reader .bi::before{background-image:url('data:image/svg+xml,')}.sidebar-navigation .quarto-reader-toggle:not(.reader) .bi::before{background-image:url('data:image/svg+xml,')}.sidebar-navigation .quarto-reader-toggle.reader .bi::before{background-image:url('data:image/svg+xml,')}#quarto-back-to-top{display:none;position:fixed;bottom:50px;background-color:#fff;border-radius:.25rem;box-shadow:0 .2rem .5rem #6c757d,0 0 .05rem #6c757d;color:#6c757d;text-decoration:none;font-size:.9em;text-align:center;left:50%;padding:.4rem .8rem;transform:translate(-50%, 0)}#quarto-announcement{padding:.5em;display:flex;justify-content:space-between;margin-bottom:0;font-size:.9em}#quarto-announcement .quarto-announcement-content{margin-right:auto}#quarto-announcement .quarto-announcement-content p{margin-bottom:0}#quarto-announcement .quarto-announcement-icon{margin-right:.5em;font-size:1.2em;margin-top:-0.15em}#quarto-announcement .quarto-announcement-action{cursor:pointer}.aa-DetachedSearchButtonQuery{display:none}.aa-DetachedOverlay ul.aa-List,#quarto-search-results ul.aa-List{list-style:none;padding-left:0}.aa-DetachedOverlay .aa-Panel,#quarto-search-results .aa-Panel{background-color:#fff;position:absolute;z-index:2000}#quarto-search-results .aa-Panel{max-width:400px}#quarto-search input{font-size:.925rem}@media(min-width: 992px){.navbar #quarto-search{margin-left:.25rem;order:999}}.navbar.navbar-expand-sm #quarto-search,.navbar.navbar-expand-md #quarto-search{order:999}@media(min-width: 992px){.navbar .quarto-navbar-tools{order:900}}@media(min-width: 992px){.navbar .quarto-navbar-tools.tools-end{margin-left:auto !important}}@media(max-width: 991.98px){#quarto-sidebar .sidebar-search{display:none}}#quarto-sidebar .sidebar-search .aa-Autocomplete{width:100%}.navbar .aa-Autocomplete .aa-Form{width:180px}.navbar #quarto-search.type-overlay .aa-Autocomplete{width:40px}.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form{background-color:inherit;border:none}.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form:focus-within{box-shadow:none;outline:none}.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form .aa-InputWrapper{display:none}.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form .aa-InputWrapper:focus-within{display:inherit}.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form .aa-Label svg,.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form .aa-LoadingIndicator svg{width:26px;height:26px;color:#fff;opacity:1}.navbar #quarto-search.type-overlay .aa-Autocomplete svg.aa-SubmitIcon{width:26px;height:26px;color:#fff;opacity:1}.aa-Autocomplete .aa-Form,.aa-DetachedFormContainer .aa-Form{align-items:center;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;color:#000;display:flex;line-height:1em;margin:0;position:relative;width:100%}.aa-Autocomplete .aa-Form:focus-within,.aa-DetachedFormContainer .aa-Form:focus-within{box-shadow:rgba(232,232,252,.6) 0 0 0 1px;outline:currentColor none medium}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix{align-items:center;display:flex;flex-shrink:0;order:1}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-Label,.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-Label,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator{cursor:initial;flex-shrink:0;padding:0;text-align:left}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-Label svg,.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator svg,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-Label svg,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator svg{color:#000;opacity:.5}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-SubmitButton,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-SubmitButton{appearance:none;background:none;border:0;margin:0}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator{align-items:center;display:flex;justify-content:center}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator[hidden],.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator[hidden]{display:none}.aa-Autocomplete .aa-Form .aa-InputWrapper,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper{order:3;position:relative;width:100%}.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input{appearance:none;background:none;border:0;color:#000;font:inherit;height:calc(1.5em + .1rem + 2px);padding:0;width:100%}.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input::placeholder,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input::placeholder{color:#000;opacity:.8}.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input:focus,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input:focus{border-color:none;box-shadow:none;outline:none}.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-decoration,.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-cancel-button,.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-results-button,.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-results-decoration,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-decoration,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-cancel-button,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-results-button,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-results-decoration{display:none}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix{align-items:center;display:flex;order:4}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-ClearButton,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-ClearButton{align-items:center;background:none;border:0;color:#000;opacity:.8;cursor:pointer;display:flex;margin:0;width:calc(1.5em + .1rem + 2px)}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-ClearButton:hover,.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-ClearButton:focus,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-ClearButton:hover,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-ClearButton:focus{color:#000;opacity:.8}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-ClearButton[hidden],.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-ClearButton[hidden]{display:none}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-ClearButton svg,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-ClearButton svg{width:calc(1.5em + 0.75rem + calc(1px * 2))}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-CopyButton,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-CopyButton{border:none;align-items:center;background:none;color:#000;opacity:.4;font-size:.7rem;cursor:pointer;display:none;margin:0;width:calc(1em + .1rem + 2px)}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-CopyButton:hover,.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-CopyButton:focus,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-CopyButton:hover,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-CopyButton:focus{color:#000;opacity:.8}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-CopyButton[hidden],.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-CopyButton[hidden]{display:none}.aa-PanelLayout:empty{display:none}.quarto-search-no-results.no-query{display:none}.aa-Source:has(.no-query){display:none}#quarto-search-results .aa-Panel{border:solid #dee2e6 1px}#quarto-search-results .aa-SourceNoResults{width:398px}.aa-DetachedOverlay .aa-Panel,#quarto-search-results .aa-Panel{max-height:65vh;overflow-y:auto;font-size:.925rem}.aa-DetachedOverlay .aa-SourceNoResults,#quarto-search-results .aa-SourceNoResults{height:60px;display:flex;justify-content:center;align-items:center}.aa-DetachedOverlay .search-error,#quarto-search-results .search-error{padding-top:10px;padding-left:20px;padding-right:20px;cursor:default}.aa-DetachedOverlay .search-error .search-error-title,#quarto-search-results .search-error .search-error-title{font-size:1.1rem;margin-bottom:.5rem}.aa-DetachedOverlay .search-error .search-error-title .search-error-icon,#quarto-search-results .search-error .search-error-title .search-error-icon{margin-right:8px}.aa-DetachedOverlay .search-error .search-error-text,#quarto-search-results .search-error .search-error-text{font-weight:300}.aa-DetachedOverlay .search-result-text,#quarto-search-results .search-result-text{font-weight:300;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;line-height:1.2rem;max-height:2.4rem}.aa-DetachedOverlay .aa-SourceHeader .search-result-header,#quarto-search-results .aa-SourceHeader .search-result-header{font-size:.875rem;background-color:#f2f2f2;padding-left:14px;padding-bottom:4px;padding-top:4px}.aa-DetachedOverlay .aa-SourceHeader .search-result-header-no-results,#quarto-search-results .aa-SourceHeader .search-result-header-no-results{display:none}.aa-DetachedOverlay .aa-SourceFooter .algolia-search-logo,#quarto-search-results .aa-SourceFooter .algolia-search-logo{width:110px;opacity:.85;margin:8px;float:right}.aa-DetachedOverlay .search-result-section,#quarto-search-results .search-result-section{font-size:.925em}.aa-DetachedOverlay a.search-result-link,#quarto-search-results a.search-result-link{color:inherit;text-decoration:none}.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item,#quarto-search-results li.aa-Item[aria-selected=true] .search-item{background-color:#e8e8fc}.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item.search-result-more,.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item .search-result-section,.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item .search-result-text,.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item .search-result-title-container,.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item .search-result-text-container,#quarto-search-results li.aa-Item[aria-selected=true] .search-item.search-result-more,#quarto-search-results li.aa-Item[aria-selected=true] .search-item .search-result-section,#quarto-search-results li.aa-Item[aria-selected=true] .search-item .search-result-text,#quarto-search-results li.aa-Item[aria-selected=true] .search-item .search-result-title-container,#quarto-search-results li.aa-Item[aria-selected=true] .search-item .search-result-text-container{color:#000;background-color:#e8e8fc}.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item mark.search-match,.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item .search-match.mark,#quarto-search-results li.aa-Item[aria-selected=true] .search-item mark.search-match,#quarto-search-results li.aa-Item[aria-selected=true] .search-item .search-match.mark{color:#000;background-color:#fff}.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item,#quarto-search-results li.aa-Item[aria-selected=false] .search-item{background-color:#fff}.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item.search-result-more,.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item .search-result-section,.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item .search-result-text,.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item .search-result-title-container,.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item .search-result-text-container,#quarto-search-results li.aa-Item[aria-selected=false] .search-item.search-result-more,#quarto-search-results li.aa-Item[aria-selected=false] .search-item .search-result-section,#quarto-search-results li.aa-Item[aria-selected=false] .search-item .search-result-text,#quarto-search-results li.aa-Item[aria-selected=false] .search-item .search-result-title-container,#quarto-search-results li.aa-Item[aria-selected=false] .search-item .search-result-text-container{color:#000}.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item mark.search-match,.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item .search-match.mark,#quarto-search-results li.aa-Item[aria-selected=false] .search-item mark.search-match,#quarto-search-results li.aa-Item[aria-selected=false] .search-item .search-match.mark{color:inherit;background-color:#fff}.aa-DetachedOverlay .aa-Item .search-result-doc:not(.document-selectable) .search-result-title-container,#quarto-search-results .aa-Item .search-result-doc:not(.document-selectable) .search-result-title-container{background-color:#fff;color:#000}.aa-DetachedOverlay .aa-Item .search-result-doc:not(.document-selectable) .search-result-text-container,#quarto-search-results .aa-Item .search-result-doc:not(.document-selectable) .search-result-text-container{padding-top:0px}.aa-DetachedOverlay li.aa-Item .search-result-doc.document-selectable .search-result-text-container,#quarto-search-results li.aa-Item .search-result-doc.document-selectable .search-result-text-container{margin-top:-4px}.aa-DetachedOverlay .aa-Item,#quarto-search-results .aa-Item{cursor:pointer}.aa-DetachedOverlay .aa-Item .search-item,#quarto-search-results .aa-Item .search-item{border-left:none;border-right:none;border-top:none;background-color:#fff;border-color:#dee2e6;color:#000}.aa-DetachedOverlay .aa-Item .search-item p,#quarto-search-results .aa-Item .search-item p{margin-top:0;margin-bottom:0}.aa-DetachedOverlay .aa-Item .search-item i.bi,#quarto-search-results .aa-Item .search-item i.bi{padding-left:8px;padding-right:8px;font-size:1.3em}.aa-DetachedOverlay .aa-Item .search-item .search-result-title,#quarto-search-results .aa-Item .search-item .search-result-title{margin-top:.3em;margin-bottom:0em}.aa-DetachedOverlay .aa-Item .search-item .search-result-crumbs,#quarto-search-results .aa-Item .search-item .search-result-crumbs{white-space:nowrap;text-overflow:ellipsis;font-size:.8em;font-weight:300;margin-right:1em}.aa-DetachedOverlay .aa-Item .search-item .search-result-crumbs:not(.search-result-crumbs-wrap),#quarto-search-results .aa-Item .search-item .search-result-crumbs:not(.search-result-crumbs-wrap){max-width:30%;margin-left:auto;margin-top:.5em;margin-bottom:.1rem}.aa-DetachedOverlay .aa-Item .search-item .search-result-crumbs.search-result-crumbs-wrap,#quarto-search-results .aa-Item .search-item .search-result-crumbs.search-result-crumbs-wrap{flex-basis:100%;margin-top:0em;margin-bottom:.2em;margin-left:37px}.aa-DetachedOverlay .aa-Item .search-result-title-container,#quarto-search-results .aa-Item .search-result-title-container{font-size:1em;display:flex;flex-wrap:wrap;padding:6px 4px 6px 4px}.aa-DetachedOverlay .aa-Item .search-result-text-container,#quarto-search-results .aa-Item .search-result-text-container{padding-bottom:8px;padding-right:8px;margin-left:42px}.aa-DetachedOverlay .aa-Item .search-result-doc-section,.aa-DetachedOverlay .aa-Item .search-result-more,#quarto-search-results .aa-Item .search-result-doc-section,#quarto-search-results .aa-Item .search-result-more{padding-top:8px;padding-bottom:8px;padding-left:44px}.aa-DetachedOverlay .aa-Item .search-result-more,#quarto-search-results .aa-Item .search-result-more{font-size:.8em;font-weight:400}.aa-DetachedOverlay .aa-Item .search-result-doc,#quarto-search-results .aa-Item .search-result-doc{border-top:1px solid #dee2e6}.aa-DetachedSearchButton{background:none;border:none}.aa-DetachedSearchButton .aa-DetachedSearchButtonPlaceholder{display:none}.navbar .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon{color:#fff}.sidebar-tools-collapse #quarto-search,.sidebar-tools-main #quarto-search{display:inline}.sidebar-tools-collapse #quarto-search .aa-Autocomplete,.sidebar-tools-main #quarto-search .aa-Autocomplete{display:inline}.sidebar-tools-collapse #quarto-search .aa-DetachedSearchButton,.sidebar-tools-main #quarto-search .aa-DetachedSearchButton{padding-left:4px;padding-right:4px}.sidebar-tools-collapse #quarto-search .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon,.sidebar-tools-main #quarto-search .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon{color:#595959}.sidebar-tools-collapse #quarto-search .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon .aa-SubmitIcon,.sidebar-tools-main #quarto-search .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon .aa-SubmitIcon{margin-top:-3px}.aa-DetachedContainer{background:rgba(255,255,255,.65);width:90%;bottom:0;box-shadow:rgba(222,226,230,.6) 0 0 0 1px;outline:currentColor none medium;display:flex;flex-direction:column;left:0;margin:0;overflow:hidden;padding:0;position:fixed;right:0;top:0;z-index:1101}.aa-DetachedContainer::after{height:32px}.aa-DetachedContainer .aa-SourceHeader{margin:var(--aa-spacing-half) 0 var(--aa-spacing-half) 2px}.aa-DetachedContainer .aa-Panel{background-color:#fff;border-radius:0;box-shadow:none;flex-grow:1;margin:0;padding:0;position:relative}.aa-DetachedContainer .aa-PanelLayout{bottom:0;box-shadow:none;left:0;margin:0;max-height:none;overflow-y:auto;position:absolute;right:0;top:0;width:100%}.aa-DetachedFormContainer{background-color:#fff;border-bottom:1px solid #dee2e6;display:flex;flex-direction:row;justify-content:space-between;margin:0;padding:.5em}.aa-DetachedCancelButton{background:none;font-size:.8em;border:0;border-radius:3px;color:#000;cursor:pointer;margin:0 0 0 .5em;padding:0 .5em}.aa-DetachedCancelButton:hover,.aa-DetachedCancelButton:focus{box-shadow:rgba(232,232,252,.6) 0 0 0 1px;outline:currentColor none medium}.aa-DetachedContainer--modal{bottom:inherit;height:auto;margin:0 auto;position:absolute;top:100px;border-radius:6px;max-width:850px}@media(max-width: 575.98px){.aa-DetachedContainer--modal{width:100%;top:0px;border-radius:0px;border:none}}.aa-DetachedContainer--modal .aa-PanelLayout{max-height:var(--aa-detached-modal-max-height);padding-bottom:var(--aa-spacing-half);position:static}.aa-Detached{height:100vh;overflow:hidden}.aa-DetachedOverlay{background-color:rgba(0,0,0,.4);position:fixed;left:0;right:0;top:0;margin:0;padding:0;height:100vh;z-index:1100}.quarto-dashboard.nav-fixed.dashboard-sidebar #quarto-content.quarto-dashboard-content{padding:0em}.quarto-dashboard #quarto-content.quarto-dashboard-content{padding:1em}.quarto-dashboard #quarto-content.quarto-dashboard-content>*{padding-top:0}@media(min-width: 576px){.quarto-dashboard{height:100%}}.quarto-dashboard .card.valuebox.bslib-card.bg-primary{background-color:#5397e9 !important}.quarto-dashboard .card.valuebox.bslib-card.bg-secondary{background-color:#343a40 !important}.quarto-dashboard .card.valuebox.bslib-card.bg-success{background-color:#3aa716 !important}.quarto-dashboard .card.valuebox.bslib-card.bg-info{background-color:rgba(153,84,187,.7019607843) !important}.quarto-dashboard .card.valuebox.bslib-card.bg-warning{background-color:#fa6400 !important}.quarto-dashboard .card.valuebox.bslib-card.bg-danger{background-color:rgba(255,0,57,.7019607843) !important}.quarto-dashboard .card.valuebox.bslib-card.bg-light{background-color:#f8f9fa !important}.quarto-dashboard .card.valuebox.bslib-card.bg-dark{background-color:#343a40 !important}.quarto-dashboard.dashboard-fill{display:flex;flex-direction:column}.quarto-dashboard #quarto-appendix{display:none}.quarto-dashboard #quarto-header #quarto-dashboard-header{border-top:solid 1px #22c472;border-bottom:solid 1px #22c472}.quarto-dashboard #quarto-header #quarto-dashboard-header>nav{padding-left:1em;padding-right:1em}.quarto-dashboard #quarto-header #quarto-dashboard-header>nav .navbar-brand-container{padding-left:0}.quarto-dashboard #quarto-header #quarto-dashboard-header .navbar-toggler{margin-right:0}.quarto-dashboard #quarto-header #quarto-dashboard-header .navbar-toggler-icon{height:1em;width:1em;background-image:url('data:image/svg+xml,')}.quarto-dashboard #quarto-header #quarto-dashboard-header .navbar-brand-container{padding-right:1em}.quarto-dashboard #quarto-header #quarto-dashboard-header .navbar-title{font-size:1.1em}.quarto-dashboard #quarto-header #quarto-dashboard-header .navbar-nav{font-size:.9em}.quarto-dashboard #quarto-dashboard-header .navbar{padding:0}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-container{padding-left:1em}.quarto-dashboard #quarto-dashboard-header .navbar.slim .navbar-brand-container .nav-link,.quarto-dashboard #quarto-dashboard-header .navbar.slim .navbar-nav .nav-link{padding:.7em}.quarto-dashboard #quarto-dashboard-header .navbar .quarto-color-scheme-toggle{order:9}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-toggler{margin-left:.5em;order:10}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-nav .nav-link{padding:.5em;height:100%;display:flex;align-items:center}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-nav .active{background-color:#24cd78}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-brand-container{padding:.5em .5em .5em 0;display:flex;flex-direction:row;margin-right:2em;align-items:center}@media(max-width: 767.98px){.quarto-dashboard #quarto-dashboard-header .navbar .navbar-brand-container{margin-right:auto}}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-collapse{align-self:stretch}@media(min-width: 768px){.quarto-dashboard #quarto-dashboard-header .navbar .navbar-collapse{order:8}}@media(max-width: 767.98px){.quarto-dashboard #quarto-dashboard-header .navbar .navbar-collapse{order:1000;padding-bottom:.5em}}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-collapse .navbar-nav{align-self:stretch}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-title{font-size:1.25em;line-height:1.1em;display:flex;flex-direction:row;flex-wrap:wrap;align-items:baseline}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-title .navbar-title-text{margin-right:.4em}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-title a{text-decoration:none;color:inherit}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-subtitle,.quarto-dashboard #quarto-dashboard-header .navbar .navbar-author{font-size:.9rem;margin-right:.5em}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-author{margin-left:auto}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-logo{max-height:48px;min-height:30px;object-fit:cover;margin-right:1em}.quarto-dashboard #quarto-dashboard-header .navbar .quarto-dashboard-links{order:9;padding-right:1em}.quarto-dashboard #quarto-dashboard-header .navbar .quarto-dashboard-link-text{margin-left:.25em}.quarto-dashboard #quarto-dashboard-header .navbar .quarto-dashboard-link{padding-right:0em;padding-left:.7em;text-decoration:none;color:#fff}.quarto-dashboard .page-layout-custom .tab-content{padding:0;border:none}.quarto-dashboard-img-contain{height:100%;width:100%;object-fit:contain}@media(max-width: 575.98px){.quarto-dashboard .bslib-grid{grid-template-rows:minmax(1em, max-content) !important}.quarto-dashboard .sidebar-content{height:inherit}.quarto-dashboard .page-layout-custom{min-height:100vh}}.quarto-dashboard.dashboard-toolbar>.page-layout-custom,.quarto-dashboard.dashboard-sidebar>.page-layout-custom{padding:0}.quarto-dashboard .quarto-dashboard-content.quarto-dashboard-pages{padding:0}.quarto-dashboard .callout{margin-bottom:0;margin-top:0}.quarto-dashboard .html-fill-container figure{overflow:hidden}.quarto-dashboard bslib-tooltip .rounded-pill{border:solid #6c757d 1px}.quarto-dashboard bslib-tooltip .rounded-pill .svg{fill:#000}.quarto-dashboard .tabset .dashboard-card-no-title .nav-tabs{margin-left:0;margin-right:auto}.quarto-dashboard .tabset .tab-content{border:none}.quarto-dashboard .tabset .card-header .nav-link[role=tab]{margin-top:-6px;padding-top:6px;padding-bottom:6px}.quarto-dashboard .card.valuebox,.quarto-dashboard .card.bslib-value-box{min-height:3rem}.quarto-dashboard .card.valuebox .card-body,.quarto-dashboard .card.bslib-value-box .card-body{padding:0}.quarto-dashboard .bslib-value-box .value-box-value{font-size:clamp(.1em,15cqw,5em)}.quarto-dashboard .bslib-value-box .value-box-showcase .bi{font-size:clamp(.1em,max(18cqw,5.2cqh),5em);text-align:center;height:1em}.quarto-dashboard .bslib-value-box .value-box-showcase .bi::before{vertical-align:1em}.quarto-dashboard .bslib-value-box .value-box-area{margin-top:auto;margin-bottom:auto}.quarto-dashboard .card figure.quarto-float{display:flex;flex-direction:column;align-items:center}.quarto-dashboard .dashboard-scrolling{padding:1em}.quarto-dashboard .full-height{height:100%}.quarto-dashboard .showcase-bottom .value-box-grid{display:grid;grid-template-columns:1fr;grid-template-rows:1fr auto;grid-template-areas:"top" "bottom"}.quarto-dashboard .showcase-bottom .value-box-grid .value-box-showcase{grid-area:bottom;padding:0;margin:0}.quarto-dashboard .showcase-bottom .value-box-grid .value-box-showcase i.bi{font-size:4rem}.quarto-dashboard .showcase-bottom .value-box-grid .value-box-area{grid-area:top}.quarto-dashboard .tab-content{margin-bottom:0}.quarto-dashboard .bslib-card .bslib-navs-card-title{justify-content:stretch;align-items:end}.quarto-dashboard .card-header{display:flex;flex-wrap:wrap;justify-content:space-between}.quarto-dashboard .card-header .card-title{display:flex;flex-direction:column;justify-content:center;margin-bottom:0}.quarto-dashboard .tabset .card-toolbar{margin-bottom:1em}.quarto-dashboard .bslib-grid>.bslib-sidebar-layout{border:none;gap:var(--bslib-spacer, 1rem)}.quarto-dashboard .bslib-grid>.bslib-sidebar-layout>.main{padding:0}.quarto-dashboard .bslib-grid>.bslib-sidebar-layout>.sidebar{border-radius:.25rem;border:1px solid rgba(0,0,0,.175)}.quarto-dashboard .bslib-grid>.bslib-sidebar-layout>.collapse-toggle{display:none}@media(max-width: 767.98px){.quarto-dashboard .bslib-grid>.bslib-sidebar-layout{grid-template-columns:1fr;grid-template-rows:max-content 1fr}.quarto-dashboard .bslib-grid>.bslib-sidebar-layout>.main{grid-column:1;grid-row:2}.quarto-dashboard .bslib-grid>.bslib-sidebar-layout .sidebar{grid-column:1;grid-row:1}}.quarto-dashboard .sidebar-right .sidebar{padding-left:2.5em}.quarto-dashboard .sidebar-right .collapse-toggle{left:2px}.quarto-dashboard .quarto-dashboard .sidebar-right button.collapse-toggle:not(.transitioning){left:unset}.quarto-dashboard aside.sidebar{padding-left:1em;padding-right:1em;background-color:rgba(52,58,64,.25);color:#000}.quarto-dashboard .bslib-sidebar-layout>div.main{padding:.7em}.quarto-dashboard .bslib-sidebar-layout button.collapse-toggle{margin-top:.3em}.quarto-dashboard .bslib-sidebar-layout .collapse-toggle{top:0}.quarto-dashboard .bslib-sidebar-layout.sidebar-collapsed:not(.transitioning):not(.sidebar-right) .collapse-toggle{left:2px}.quarto-dashboard .sidebar>section>.h3:first-of-type{margin-top:0em}.quarto-dashboard .sidebar .h3,.quarto-dashboard .sidebar .h4,.quarto-dashboard .sidebar .h5,.quarto-dashboard .sidebar .h6{margin-top:.5em}.quarto-dashboard .sidebar form{flex-direction:column;align-items:start;margin-bottom:1em}.quarto-dashboard .sidebar form div[class*=oi-][class$=-input]{flex-direction:column}.quarto-dashboard .sidebar form[class*=oi-][class$=-toggle]{flex-direction:row-reverse;align-items:center;justify-content:start}.quarto-dashboard .sidebar form input[type=range]{margin-top:.5em;margin-right:.8em;margin-left:1em}.quarto-dashboard .sidebar label{width:fit-content}.quarto-dashboard .sidebar .card-body{margin-bottom:2em}.quarto-dashboard .sidebar .shiny-input-container{margin-bottom:1em}.quarto-dashboard .sidebar .shiny-options-group{margin-top:0}.quarto-dashboard .sidebar .control-label{margin-bottom:.3em}.quarto-dashboard .card .card-body .quarto-layout-row{align-items:stretch}.quarto-dashboard .toolbar{font-size:.9em;display:flex;flex-direction:row;border-top:solid 1px #bcbfc0;padding:1em;flex-wrap:wrap;background-color:rgba(52,58,64,.25)}.quarto-dashboard .toolbar .cell-output-display{display:flex}.quarto-dashboard .toolbar .shiny-input-container{padding-bottom:.5em;margin-bottom:.5em;width:inherit}.quarto-dashboard .toolbar .shiny-input-container>.checkbox:first-child{margin-top:6px}.quarto-dashboard .toolbar>*:last-child{margin-right:0}.quarto-dashboard .toolbar>*>*{margin-right:1em;align-items:baseline}.quarto-dashboard .toolbar>*>*>a{text-decoration:none;margin-top:auto;margin-bottom:auto}.quarto-dashboard .toolbar .shiny-input-container{padding-bottom:0;margin-bottom:0}.quarto-dashboard .toolbar .shiny-input-container>*{flex-shrink:0;flex-grow:0}.quarto-dashboard .toolbar .form-group.shiny-input-container:not([role=group])>label{margin-bottom:0}.quarto-dashboard .toolbar .shiny-input-container.no-baseline{align-items:start;padding-top:6px}.quarto-dashboard .toolbar .shiny-input-container{display:flex;align-items:baseline}.quarto-dashboard .toolbar .shiny-input-container label{padding-right:.4em}.quarto-dashboard .toolbar .shiny-input-container .bslib-input-switch{margin-top:6px}.quarto-dashboard .toolbar input[type=text]{line-height:1;width:inherit}.quarto-dashboard .toolbar .input-daterange{width:inherit}.quarto-dashboard .toolbar .input-daterange input[type=text]{height:2.4em;width:10em}.quarto-dashboard .toolbar .input-daterange .input-group-addon{height:auto;padding:0;margin-left:-5px !important;margin-right:-5px}.quarto-dashboard .toolbar .input-daterange .input-group-addon .input-group-text{padding-top:0;padding-bottom:0;height:100%}.quarto-dashboard .toolbar span.irs.irs--shiny{width:10em}.quarto-dashboard .toolbar span.irs.irs--shiny .irs-line{top:9px}.quarto-dashboard .toolbar span.irs.irs--shiny .irs-min,.quarto-dashboard .toolbar span.irs.irs--shiny .irs-max,.quarto-dashboard .toolbar span.irs.irs--shiny .irs-from,.quarto-dashboard .toolbar span.irs.irs--shiny .irs-to,.quarto-dashboard .toolbar span.irs.irs--shiny .irs-single{top:20px}.quarto-dashboard .toolbar span.irs.irs--shiny .irs-bar{top:8px}.quarto-dashboard .toolbar span.irs.irs--shiny .irs-handle{top:0px}.quarto-dashboard .toolbar .shiny-input-checkboxgroup>label{margin-top:6px}.quarto-dashboard .toolbar .shiny-input-checkboxgroup>.shiny-options-group{margin-top:0;align-items:baseline}.quarto-dashboard .toolbar .shiny-input-radiogroup>label{margin-top:6px}.quarto-dashboard .toolbar .shiny-input-radiogroup>.shiny-options-group{align-items:baseline;margin-top:0}.quarto-dashboard .toolbar .shiny-input-radiogroup>.shiny-options-group>.radio{margin-right:.3em}.quarto-dashboard .toolbar .form-select{padding-top:.2em;padding-bottom:.2em}.quarto-dashboard .toolbar .shiny-input-select{min-width:6em}.quarto-dashboard .toolbar div.checkbox{margin-bottom:0px}.quarto-dashboard .toolbar>.checkbox:first-child{margin-top:6px}.quarto-dashboard .toolbar form{width:fit-content}.quarto-dashboard .toolbar form label{padding-top:.2em;padding-bottom:.2em;width:fit-content}.quarto-dashboard .toolbar form input[type=date]{width:fit-content}.quarto-dashboard .toolbar form input[type=color]{width:3em}.quarto-dashboard .toolbar form button{padding:.4em}.quarto-dashboard .toolbar form select{width:fit-content}.quarto-dashboard .toolbar>*{font-size:.9em;flex-grow:0}.quarto-dashboard .toolbar .shiny-input-container label{margin-bottom:1px}.quarto-dashboard .toolbar-bottom{margin-top:1em;margin-bottom:0 !important;order:2}.quarto-dashboard .quarto-dashboard-content>.dashboard-toolbar-container>.toolbar-content>.tab-content>.tab-pane>*:not(.bslib-sidebar-layout){padding:1em}.quarto-dashboard .quarto-dashboard-content>.dashboard-toolbar-container>.toolbar-content>*:not(.tab-content){padding:1em}.quarto-dashboard .quarto-dashboard-content>.tab-content>.dashboard-page>.dashboard-toolbar-container>.toolbar-content,.quarto-dashboard .quarto-dashboard-content>.tab-content>.dashboard-page:not(.dashboard-sidebar-container)>*:not(.dashboard-toolbar-container){padding:1em}.quarto-dashboard .toolbar-content{padding:0}.quarto-dashboard .quarto-dashboard-content.quarto-dashboard-pages .tab-pane>.dashboard-toolbar-container .toolbar{border-radius:0;margin-bottom:0}.quarto-dashboard .dashboard-toolbar-container.toolbar-toplevel .toolbar{border-bottom:1px solid rgba(0,0,0,.175)}.quarto-dashboard .dashboard-toolbar-container.toolbar-toplevel .toolbar-bottom{margin-top:0}.quarto-dashboard .dashboard-toolbar-container:not(.toolbar-toplevel) .toolbar{margin-bottom:1em;border-top:none;border-radius:.25rem;border:1px solid rgba(0,0,0,.175)}.quarto-dashboard .vega-embed.has-actions details{width:1.7em;height:2em;position:absolute !important;top:0;right:0}.quarto-dashboard .dashboard-toolbar-container{padding:0}.quarto-dashboard .card .card-header p:last-child,.quarto-dashboard .card .card-footer p:last-child{margin-bottom:0}.quarto-dashboard .card .card-body>.h4:first-child{margin-top:0}.quarto-dashboard .card .card-body{z-index:4}@media(max-width: 767.98px){.quarto-dashboard .card .card-body .itables div.dataTables_wrapper div.dataTables_length,.quarto-dashboard .card .card-body .itables div.dataTables_wrapper div.dataTables_info,.quarto-dashboard .card .card-body .itables div.dataTables_wrapper div.dataTables_paginate{text-align:initial}.quarto-dashboard .card .card-body .itables div.dataTables_wrapper div.dataTables_filter{text-align:right}.quarto-dashboard .card .card-body .itables div.dataTables_wrapper div.dataTables_paginate ul.pagination{justify-content:initial}}.quarto-dashboard .card .card-body .itables .dataTables_wrapper{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;padding-top:0}.quarto-dashboard .card .card-body .itables .dataTables_wrapper table{flex-shrink:0}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dt-buttons{margin-bottom:.5em;margin-left:auto;width:fit-content;float:right}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dt-buttons.btn-group{background:#fff;border:none}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dt-buttons .btn-secondary{background-color:#fff;background-image:none;border:solid #dee2e6 1px;padding:.2em .7em}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dt-buttons .btn span{font-size:.8em;color:#000}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_info{margin-left:.5em;margin-bottom:.5em;padding-top:0}@media(min-width: 768px){.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_info{font-size:.875em}}@media(max-width: 767.98px){.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_info{font-size:.8em}}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_filter{margin-bottom:.5em;font-size:.875em}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_filter input[type=search]{padding:1px 5px 1px 5px;font-size:.875em}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_length{flex-basis:1 1 50%;margin-bottom:.5em;font-size:.875em}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_length select{padding:.4em 3em .4em .5em;font-size:.875em;margin-left:.2em;margin-right:.2em}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_paginate{flex-shrink:0}@media(min-width: 768px){.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_paginate{margin-left:auto}}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_paginate ul.pagination .paginate_button .page-link{font-size:.8em}.quarto-dashboard .card .card-footer{font-size:.9em}.quarto-dashboard .card .card-toolbar{display:flex;flex-grow:1;flex-direction:row;width:100%;flex-wrap:wrap}.quarto-dashboard .card .card-toolbar>*{font-size:.8em;flex-grow:0}.quarto-dashboard .card .card-toolbar>.card-title{font-size:1em;flex-grow:1;align-self:flex-start;margin-top:.1em}.quarto-dashboard .card .card-toolbar .cell-output-display{display:flex}.quarto-dashboard .card .card-toolbar .shiny-input-container{padding-bottom:.5em;margin-bottom:.5em;width:inherit}.quarto-dashboard .card .card-toolbar .shiny-input-container>.checkbox:first-child{margin-top:6px}.quarto-dashboard .card .card-toolbar>*:last-child{margin-right:0}.quarto-dashboard .card .card-toolbar>*>*{margin-right:1em;align-items:baseline}.quarto-dashboard .card .card-toolbar>*>*>a{text-decoration:none;margin-top:auto;margin-bottom:auto}.quarto-dashboard .card .card-toolbar form{width:fit-content}.quarto-dashboard .card .card-toolbar form label{padding-top:.2em;padding-bottom:.2em;width:fit-content}.quarto-dashboard .card .card-toolbar form input[type=date]{width:fit-content}.quarto-dashboard .card .card-toolbar form input[type=color]{width:3em}.quarto-dashboard .card .card-toolbar form button{padding:.4em}.quarto-dashboard .card .card-toolbar form select{width:fit-content}.quarto-dashboard .card .card-toolbar .cell-output-display{display:flex}.quarto-dashboard .card .card-toolbar .shiny-input-container{padding-bottom:.5em;margin-bottom:.5em;width:inherit}.quarto-dashboard .card .card-toolbar .shiny-input-container>.checkbox:first-child{margin-top:6px}.quarto-dashboard .card .card-toolbar>*:last-child{margin-right:0}.quarto-dashboard .card .card-toolbar>*>*{margin-right:1em;align-items:baseline}.quarto-dashboard .card .card-toolbar>*>*>a{text-decoration:none;margin-top:auto;margin-bottom:auto}.quarto-dashboard .card .card-toolbar .shiny-input-container{padding-bottom:0;margin-bottom:0}.quarto-dashboard .card .card-toolbar .shiny-input-container>*{flex-shrink:0;flex-grow:0}.quarto-dashboard .card .card-toolbar .form-group.shiny-input-container:not([role=group])>label{margin-bottom:0}.quarto-dashboard .card .card-toolbar .shiny-input-container.no-baseline{align-items:start;padding-top:6px}.quarto-dashboard .card .card-toolbar .shiny-input-container{display:flex;align-items:baseline}.quarto-dashboard .card .card-toolbar .shiny-input-container label{padding-right:.4em}.quarto-dashboard .card .card-toolbar .shiny-input-container .bslib-input-switch{margin-top:6px}.quarto-dashboard .card .card-toolbar input[type=text]{line-height:1;width:inherit}.quarto-dashboard .card .card-toolbar .input-daterange{width:inherit}.quarto-dashboard .card .card-toolbar .input-daterange input[type=text]{height:2.4em;width:10em}.quarto-dashboard .card .card-toolbar .input-daterange .input-group-addon{height:auto;padding:0;margin-left:-5px !important;margin-right:-5px}.quarto-dashboard .card .card-toolbar .input-daterange .input-group-addon .input-group-text{padding-top:0;padding-bottom:0;height:100%}.quarto-dashboard .card .card-toolbar span.irs.irs--shiny{width:10em}.quarto-dashboard .card .card-toolbar span.irs.irs--shiny .irs-line{top:9px}.quarto-dashboard .card .card-toolbar span.irs.irs--shiny .irs-min,.quarto-dashboard .card .card-toolbar span.irs.irs--shiny .irs-max,.quarto-dashboard .card .card-toolbar span.irs.irs--shiny .irs-from,.quarto-dashboard .card .card-toolbar span.irs.irs--shiny .irs-to,.quarto-dashboard .card .card-toolbar span.irs.irs--shiny .irs-single{top:20px}.quarto-dashboard .card .card-toolbar span.irs.irs--shiny .irs-bar{top:8px}.quarto-dashboard .card .card-toolbar span.irs.irs--shiny .irs-handle{top:0px}.quarto-dashboard .card .card-toolbar .shiny-input-checkboxgroup>label{margin-top:6px}.quarto-dashboard .card .card-toolbar .shiny-input-checkboxgroup>.shiny-options-group{margin-top:0;align-items:baseline}.quarto-dashboard .card .card-toolbar .shiny-input-radiogroup>label{margin-top:6px}.quarto-dashboard .card .card-toolbar .shiny-input-radiogroup>.shiny-options-group{align-items:baseline;margin-top:0}.quarto-dashboard .card .card-toolbar .shiny-input-radiogroup>.shiny-options-group>.radio{margin-right:.3em}.quarto-dashboard .card .card-toolbar .form-select{padding-top:.2em;padding-bottom:.2em}.quarto-dashboard .card .card-toolbar .shiny-input-select{min-width:6em}.quarto-dashboard .card .card-toolbar div.checkbox{margin-bottom:0px}.quarto-dashboard .card .card-toolbar>.checkbox:first-child{margin-top:6px}.quarto-dashboard .card-body>table>thead{border-top:none}.quarto-dashboard .card-body>.table>:not(caption)>*>*{background-color:#fff}.tableFloatingHeaderOriginal{background-color:#fff;position:sticky !important;top:0 !important}.dashboard-data-table{margin-top:-1px}div.value-box-area span.observablehq--number{font-size:calc(clamp(.1em,15cqw,5em)*1.25);line-height:1.2;color:inherit;font-family:var(--bs-body-font-family)}.quarto-listing{padding-bottom:1em}.listing-pagination{padding-top:.5em}ul.pagination{float:right;padding-left:8px;padding-top:.5em}ul.pagination li{padding-right:.75em}ul.pagination li.disabled a,ul.pagination li.active a{color:#000;text-decoration:none}ul.pagination li:last-of-type{padding-right:0}.listing-actions-group{display:flex}.quarto-listing-filter{margin-bottom:1em;width:200px;margin-left:auto}.quarto-listing-sort{margin-bottom:1em;margin-right:auto;width:auto}.quarto-listing-sort .input-group-text{font-size:.8em}.input-group-text{border-right:none}.quarto-listing-sort select.form-select{font-size:.8em}.listing-no-matching{text-align:center;padding-top:2em;padding-bottom:3em;font-size:1em}#quarto-margin-sidebar .quarto-listing-category{padding-top:0;font-size:1rem}#quarto-margin-sidebar .quarto-listing-category-title{cursor:pointer;font-weight:600;font-size:1rem}.quarto-listing-category .category{cursor:pointer}.quarto-listing-category .category.active{font-weight:600}.quarto-listing-category.category-cloud{display:flex;flex-wrap:wrap;align-items:baseline}.quarto-listing-category.category-cloud .category{padding-right:5px}.quarto-listing-category.category-cloud .category-cloud-1{font-size:.75em}.quarto-listing-category.category-cloud .category-cloud-2{font-size:.95em}.quarto-listing-category.category-cloud .category-cloud-3{font-size:1.15em}.quarto-listing-category.category-cloud .category-cloud-4{font-size:1.35em}.quarto-listing-category.category-cloud .category-cloud-5{font-size:1.55em}.quarto-listing-category.category-cloud .category-cloud-6{font-size:1.75em}.quarto-listing-category.category-cloud .category-cloud-7{font-size:1.95em}.quarto-listing-category.category-cloud .category-cloud-8{font-size:2.15em}.quarto-listing-category.category-cloud .category-cloud-9{font-size:2.35em}.quarto-listing-category.category-cloud .category-cloud-10{font-size:2.55em}.quarto-listing-cols-1{grid-template-columns:repeat(1, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-1{grid-template-columns:repeat(1, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-1{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-2{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-3{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-3{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-4{grid-template-columns:repeat(4, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-4{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-4{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-5{grid-template-columns:repeat(5, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-5{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-5{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-6{grid-template-columns:repeat(6, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-6{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-6{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-7{grid-template-columns:repeat(7, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-7{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-7{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-8{grid-template-columns:repeat(8, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-8{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-8{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-9{grid-template-columns:repeat(9, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-9{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-9{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-10{grid-template-columns:repeat(10, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-10{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-10{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-11{grid-template-columns:repeat(11, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-11{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-11{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-12{grid-template-columns:repeat(12, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-12{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-12{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-grid{gap:1.5em}.quarto-grid-item.borderless{border:none}.quarto-grid-item.borderless .listing-categories .listing-category:last-of-type,.quarto-grid-item.borderless .listing-categories .listing-category:first-of-type{padding-left:0}.quarto-grid-item.borderless .listing-categories .listing-category{border:0}.quarto-grid-link{text-decoration:none;color:inherit}.quarto-grid-link:hover{text-decoration:none;color:inherit}.quarto-grid-item h5.title,.quarto-grid-item .title.h5{margin-top:0;margin-bottom:0}.quarto-grid-item .card-footer{display:flex;justify-content:space-between;font-size:.8em}.quarto-grid-item .card-footer p{margin-bottom:0}.quarto-grid-item p.card-img-top{margin-bottom:0}.quarto-grid-item p.card-img-top>img{object-fit:cover}.quarto-grid-item .card-other-values{margin-top:.5em;font-size:.8em}.quarto-grid-item .card-other-values tr{margin-bottom:.5em}.quarto-grid-item .card-other-values tr>td:first-of-type{font-weight:600;padding-right:1em;padding-left:1em;vertical-align:top}.quarto-grid-item div.post-contents{display:flex;flex-direction:column;text-decoration:none;height:100%}.quarto-grid-item .listing-item-img-placeholder{background-color:rgba(52,58,64,.25);flex-shrink:0}.quarto-grid-item .card-attribution{padding-top:1em;display:flex;gap:1em;text-transform:uppercase;color:#6c757d;font-weight:500;flex-grow:10;align-items:flex-end}.quarto-grid-item .description{padding-bottom:1em}.quarto-grid-item .card-attribution .date{align-self:flex-end}.quarto-grid-item .card-attribution.justify{justify-content:space-between}.quarto-grid-item .card-attribution.start{justify-content:flex-start}.quarto-grid-item .card-attribution.end{justify-content:flex-end}.quarto-grid-item .card-title{margin-bottom:.1em}.quarto-grid-item .card-subtitle{padding-top:.25em}.quarto-grid-item .card-text{font-size:.9em}.quarto-grid-item .listing-reading-time{padding-bottom:.25em}.quarto-grid-item .card-text-small{font-size:.8em}.quarto-grid-item .card-subtitle.subtitle{font-size:.9em;font-weight:600;padding-bottom:.5em}.quarto-grid-item .listing-categories{display:flex;flex-wrap:wrap;padding-bottom:5px}.quarto-grid-item .listing-categories .listing-category{color:#6c757d;border:solid 1px #dee2e6;border-radius:.25rem;text-transform:uppercase;font-size:.65em;padding-left:.5em;padding-right:.5em;padding-top:.15em;padding-bottom:.15em;cursor:pointer;margin-right:4px;margin-bottom:4px}.quarto-grid-item.card-right{text-align:right}.quarto-grid-item.card-right .listing-categories{justify-content:flex-end}.quarto-grid-item.card-left{text-align:left}.quarto-grid-item.card-center{text-align:center}.quarto-grid-item.card-center .listing-description{text-align:justify}.quarto-grid-item.card-center .listing-categories{justify-content:center}table.quarto-listing-table td.image{padding:0px}table.quarto-listing-table td.image img{width:100%;max-width:50px;object-fit:contain}table.quarto-listing-table a{text-decoration:none;word-break:keep-all}table.quarto-listing-table th a{color:inherit}table.quarto-listing-table th a.asc:after{margin-bottom:-2px;margin-left:5px;display:inline-block;height:1rem;width:1rem;background-repeat:no-repeat;background-size:1rem 1rem;background-image:url('data:image/svg+xml,');content:""}table.quarto-listing-table th a.desc:after{margin-bottom:-2px;margin-left:5px;display:inline-block;height:1rem;width:1rem;background-repeat:no-repeat;background-size:1rem 1rem;background-image:url('data:image/svg+xml,');content:""}table.quarto-listing-table.table-hover td{cursor:pointer}.quarto-post.image-left{flex-direction:row}.quarto-post.image-right{flex-direction:row-reverse}@media(max-width: 767.98px){.quarto-post.image-right,.quarto-post.image-left{gap:0em;flex-direction:column}.quarto-post .metadata{padding-bottom:1em;order:2}.quarto-post .body{order:1}.quarto-post .thumbnail{order:3}}.list.quarto-listing-default div:last-of-type{border-bottom:none}@media(min-width: 992px){.quarto-listing-container-default{margin-right:2em}}div.quarto-post{display:flex;gap:2em;margin-bottom:1.5em;border-bottom:1px solid #dee2e6}@media(max-width: 767.98px){div.quarto-post{padding-bottom:1em}}div.quarto-post .metadata{flex-basis:20%;flex-grow:0;margin-top:.2em;flex-shrink:10}div.quarto-post .thumbnail{flex-basis:30%;flex-grow:0;flex-shrink:0}div.quarto-post .thumbnail img{margin-top:.4em;width:100%;object-fit:cover}div.quarto-post .body{flex-basis:45%;flex-grow:1;flex-shrink:0}div.quarto-post .body h3.listing-title,div.quarto-post .body .listing-title.h3{margin-top:0px;margin-bottom:0px;border-bottom:none}div.quarto-post .body .listing-subtitle{font-size:.875em;margin-bottom:.5em;margin-top:.2em}div.quarto-post .body .description{font-size:.9em}div.quarto-post .body pre code{white-space:pre-wrap}div.quarto-post a{color:#000;text-decoration:none}div.quarto-post .metadata{display:flex;flex-direction:column;font-size:.8em;font-family:"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";flex-basis:33%}div.quarto-post .listing-categories{display:flex;flex-wrap:wrap;padding-bottom:5px}div.quarto-post .listing-categories .listing-category{color:#6c757d;border:solid 1px #dee2e6;border-radius:.25rem;text-transform:uppercase;font-size:.65em;padding-left:.5em;padding-right:.5em;padding-top:.15em;padding-bottom:.15em;cursor:pointer;margin-right:4px;margin-bottom:4px}div.quarto-post .listing-description{margin-bottom:.5em}div.quarto-about-jolla{display:flex !important;flex-direction:column;align-items:center;margin-top:10%;padding-bottom:1em}div.quarto-about-jolla .about-image{object-fit:cover;margin-left:auto;margin-right:auto;margin-bottom:1.5em}div.quarto-about-jolla img.round{border-radius:50%}div.quarto-about-jolla img.rounded{border-radius:10px}div.quarto-about-jolla .quarto-title h1.title,div.quarto-about-jolla .quarto-title .title.h1{text-align:center}div.quarto-about-jolla .quarto-title .description{text-align:center}div.quarto-about-jolla h2,div.quarto-about-jolla .h2{border-bottom:none}div.quarto-about-jolla .about-sep{width:60%}div.quarto-about-jolla main{text-align:center}div.quarto-about-jolla .about-links{display:flex}@media(min-width: 992px){div.quarto-about-jolla .about-links{flex-direction:row;column-gap:.8em;row-gap:15px;flex-wrap:wrap}}@media(max-width: 991.98px){div.quarto-about-jolla .about-links{flex-direction:column;row-gap:1em;width:100%;padding-bottom:1.5em}}div.quarto-about-jolla .about-link{color:#333;text-decoration:none;border:solid 1px}@media(min-width: 992px){div.quarto-about-jolla .about-link{font-size:.8em;padding:.25em .5em;border-radius:4px}}@media(max-width: 991.98px){div.quarto-about-jolla .about-link{font-size:1.1em;padding:.5em .5em;text-align:center;border-radius:6px}}div.quarto-about-jolla .about-link:hover{color:#4040bf}div.quarto-about-jolla .about-link i.bi{margin-right:.15em}div.quarto-about-solana{display:flex !important;flex-direction:column;padding-top:3em !important;padding-bottom:1em}div.quarto-about-solana .about-entity{display:flex !important;align-items:start;justify-content:space-between}@media(min-width: 992px){div.quarto-about-solana .about-entity{flex-direction:row}}@media(max-width: 991.98px){div.quarto-about-solana .about-entity{flex-direction:column-reverse;align-items:center;text-align:center}}div.quarto-about-solana .about-entity .entity-contents{display:flex;flex-direction:column}@media(max-width: 767.98px){div.quarto-about-solana .about-entity .entity-contents{width:100%}}div.quarto-about-solana .about-entity .about-image{object-fit:cover}@media(max-width: 991.98px){div.quarto-about-solana .about-entity .about-image{margin-bottom:1.5em}}div.quarto-about-solana .about-entity img.round{border-radius:50%}div.quarto-about-solana .about-entity img.rounded{border-radius:10px}div.quarto-about-solana .about-entity .about-links{display:flex;justify-content:left;padding-bottom:1.2em}@media(min-width: 992px){div.quarto-about-solana .about-entity .about-links{flex-direction:row;column-gap:.8em;row-gap:15px;flex-wrap:wrap}}@media(max-width: 991.98px){div.quarto-about-solana .about-entity .about-links{flex-direction:column;row-gap:1em;width:100%;padding-bottom:1.5em}}div.quarto-about-solana .about-entity .about-link{color:#333;text-decoration:none;border:solid 1px}@media(min-width: 992px){div.quarto-about-solana .about-entity .about-link{font-size:.8em;padding:.25em .5em;border-radius:4px}}@media(max-width: 991.98px){div.quarto-about-solana .about-entity .about-link{font-size:1.1em;padding:.5em .5em;text-align:center;border-radius:6px}}div.quarto-about-solana .about-entity .about-link:hover{color:#4040bf}div.quarto-about-solana .about-entity .about-link i.bi{margin-right:.15em}div.quarto-about-solana .about-contents{padding-right:1.5em;flex-basis:0;flex-grow:1}div.quarto-about-solana .about-contents main.content{margin-top:0}div.quarto-about-solana .about-contents h2,div.quarto-about-solana .about-contents .h2{border-bottom:none}div.quarto-about-trestles{display:flex !important;flex-direction:row;padding-top:3em !important;padding-bottom:1em}@media(max-width: 991.98px){div.quarto-about-trestles{flex-direction:column;padding-top:0em !important}}div.quarto-about-trestles .about-entity{display:flex !important;flex-direction:column;align-items:center;text-align:center;padding-right:1em}@media(min-width: 992px){div.quarto-about-trestles .about-entity{flex:0 0 42%}}div.quarto-about-trestles .about-entity .about-image{object-fit:cover;margin-bottom:1.5em}div.quarto-about-trestles .about-entity img.round{border-radius:50%}div.quarto-about-trestles .about-entity img.rounded{border-radius:10px}div.quarto-about-trestles .about-entity .about-links{display:flex;justify-content:center}@media(min-width: 992px){div.quarto-about-trestles .about-entity .about-links{flex-direction:row;column-gap:.8em;row-gap:15px;flex-wrap:wrap}}@media(max-width: 991.98px){div.quarto-about-trestles .about-entity .about-links{flex-direction:column;row-gap:1em;width:100%;padding-bottom:1.5em}}div.quarto-about-trestles .about-entity .about-link{color:#333;text-decoration:none;border:solid 1px}@media(min-width: 992px){div.quarto-about-trestles .about-entity .about-link{font-size:.8em;padding:.25em .5em;border-radius:4px}}@media(max-width: 991.98px){div.quarto-about-trestles .about-entity .about-link{font-size:1.1em;padding:.5em .5em;text-align:center;border-radius:6px}}div.quarto-about-trestles .about-entity .about-link:hover{color:#4040bf}div.quarto-about-trestles .about-entity .about-link i.bi{margin-right:.15em}div.quarto-about-trestles .about-contents{flex-basis:0;flex-grow:1}div.quarto-about-trestles .about-contents h2,div.quarto-about-trestles .about-contents .h2{border-bottom:none}@media(min-width: 992px){div.quarto-about-trestles .about-contents{border-left:solid 1px #dee2e6;padding-left:1.5em}}div.quarto-about-trestles .about-contents main.content{margin-top:0}div.quarto-about-marquee{padding-bottom:1em}div.quarto-about-marquee .about-contents{display:flex;flex-direction:column}div.quarto-about-marquee .about-image{max-height:550px;margin-bottom:1.5em;object-fit:cover}div.quarto-about-marquee img.round{border-radius:50%}div.quarto-about-marquee img.rounded{border-radius:10px}div.quarto-about-marquee h2,div.quarto-about-marquee .h2{border-bottom:none}div.quarto-about-marquee .about-links{display:flex;justify-content:center;padding-top:1.5em}@media(min-width: 992px){div.quarto-about-marquee .about-links{flex-direction:row;column-gap:.8em;row-gap:15px;flex-wrap:wrap}}@media(max-width: 991.98px){div.quarto-about-marquee .about-links{flex-direction:column;row-gap:1em;width:100%;padding-bottom:1.5em}}div.quarto-about-marquee .about-link{color:#333;text-decoration:none;border:solid 1px}@media(min-width: 992px){div.quarto-about-marquee .about-link{font-size:.8em;padding:.25em .5em;border-radius:4px}}@media(max-width: 991.98px){div.quarto-about-marquee .about-link{font-size:1.1em;padding:.5em .5em;text-align:center;border-radius:6px}}div.quarto-about-marquee .about-link:hover{color:#4040bf}div.quarto-about-marquee .about-link i.bi{margin-right:.15em}@media(min-width: 992px){div.quarto-about-marquee .about-link{border:none}}div.quarto-about-broadside{display:flex;flex-direction:column;padding-bottom:1em}div.quarto-about-broadside .about-main{display:flex !important;padding-top:0 !important}@media(min-width: 992px){div.quarto-about-broadside .about-main{flex-direction:row;align-items:flex-start}}@media(max-width: 991.98px){div.quarto-about-broadside .about-main{flex-direction:column}}@media(max-width: 991.98px){div.quarto-about-broadside .about-main .about-entity{flex-shrink:0;width:100%;height:450px;margin-bottom:1.5em;background-size:cover;background-repeat:no-repeat}}@media(min-width: 992px){div.quarto-about-broadside .about-main .about-entity{flex:0 10 50%;margin-right:1.5em;width:100%;height:100%;background-size:100%;background-repeat:no-repeat}}div.quarto-about-broadside .about-main .about-contents{padding-top:14px;flex:0 0 50%}div.quarto-about-broadside h2,div.quarto-about-broadside .h2{border-bottom:none}div.quarto-about-broadside .about-sep{margin-top:1.5em;width:60%;align-self:center}div.quarto-about-broadside .about-links{display:flex;justify-content:center;column-gap:20px;padding-top:1.5em}@media(min-width: 992px){div.quarto-about-broadside .about-links{flex-direction:row;column-gap:.8em;row-gap:15px;flex-wrap:wrap}}@media(max-width: 991.98px){div.quarto-about-broadside .about-links{flex-direction:column;row-gap:1em;width:100%;padding-bottom:1.5em}}div.quarto-about-broadside .about-link{color:#333;text-decoration:none;border:solid 1px}@media(min-width: 992px){div.quarto-about-broadside .about-link{font-size:.8em;padding:.25em .5em;border-radius:4px}}@media(max-width: 991.98px){div.quarto-about-broadside .about-link{font-size:1.1em;padding:.5em .5em;text-align:center;border-radius:6px}}div.quarto-about-broadside .about-link:hover{color:#4040bf}div.quarto-about-broadside .about-link i.bi{margin-right:.15em}@media(min-width: 992px){div.quarto-about-broadside .about-link{border:none}}.tippy-box[data-theme~=quarto]{background-color:#fff;border:solid 1px #dee2e6;border-radius:.25rem;color:#000;font-size:.875rem}.tippy-box[data-theme~=quarto]>.tippy-backdrop{background-color:#fff}.tippy-box[data-theme~=quarto]>.tippy-arrow:after,.tippy-box[data-theme~=quarto]>.tippy-svg-arrow:after{content:"";position:absolute;z-index:-1}.tippy-box[data-theme~=quarto]>.tippy-arrow:after{border-color:rgba(0,0,0,0);border-style:solid}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-6px}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-6px}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-6px}.tippy-box[data-placement^=left]>.tippy-arrow:before{right:-6px}.tippy-box[data-theme~=quarto][data-placement^=top]>.tippy-arrow:before{border-top-color:#fff}.tippy-box[data-theme~=quarto][data-placement^=top]>.tippy-arrow:after{border-top-color:#dee2e6;border-width:7px 7px 0;top:17px;left:1px}.tippy-box[data-theme~=quarto][data-placement^=top]>.tippy-svg-arrow>svg{top:16px}.tippy-box[data-theme~=quarto][data-placement^=top]>.tippy-svg-arrow:after{top:17px}.tippy-box[data-theme~=quarto][data-placement^=bottom]>.tippy-arrow:before{border-bottom-color:#fff;bottom:16px}.tippy-box[data-theme~=quarto][data-placement^=bottom]>.tippy-arrow:after{border-bottom-color:#dee2e6;border-width:0 7px 7px;bottom:17px;left:1px}.tippy-box[data-theme~=quarto][data-placement^=bottom]>.tippy-svg-arrow>svg{bottom:15px}.tippy-box[data-theme~=quarto][data-placement^=bottom]>.tippy-svg-arrow:after{bottom:17px}.tippy-box[data-theme~=quarto][data-placement^=left]>.tippy-arrow:before{border-left-color:#fff}.tippy-box[data-theme~=quarto][data-placement^=left]>.tippy-arrow:after{border-left-color:#dee2e6;border-width:7px 0 7px 7px;left:17px;top:1px}.tippy-box[data-theme~=quarto][data-placement^=left]>.tippy-svg-arrow>svg{left:11px}.tippy-box[data-theme~=quarto][data-placement^=left]>.tippy-svg-arrow:after{left:12px}.tippy-box[data-theme~=quarto][data-placement^=right]>.tippy-arrow:before{border-right-color:#fff;right:16px}.tippy-box[data-theme~=quarto][data-placement^=right]>.tippy-arrow:after{border-width:7px 7px 7px 0;right:17px;top:1px;border-right-color:#dee2e6}.tippy-box[data-theme~=quarto][data-placement^=right]>.tippy-svg-arrow>svg{right:11px}.tippy-box[data-theme~=quarto][data-placement^=right]>.tippy-svg-arrow:after{right:12px}.tippy-box[data-theme~=quarto]>.tippy-svg-arrow{fill:#000}.tippy-box[data-theme~=quarto]>.tippy-svg-arrow:after{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMCA2czEuNzk2LS4wMTMgNC42Ny0zLjYxNUM1Ljg1MS45IDYuOTMuMDA2IDggMGMxLjA3LS4wMDYgMi4xNDguODg3IDMuMzQzIDIuMzg1QzE0LjIzMyA2LjAwNSAxNiA2IDE2IDZIMHoiIGZpbGw9InJnYmEoMCwgOCwgMTYsIDAuMikiLz48L3N2Zz4=);background-size:16px 6px;width:16px;height:6px}.top-right{position:absolute;top:1em;right:1em}.visually-hidden{border:0;clip:rect(0 0 0 0);height:auto;margin:0;overflow:hidden;padding:0;position:absolute;width:1px;white-space:nowrap}.hidden{display:none !important}.zindex-bottom{z-index:-1 !important}figure.figure{display:block}.quarto-layout-panel{margin-bottom:1em}.quarto-layout-panel>figure{width:100%}.quarto-layout-panel>figure>figcaption,.quarto-layout-panel>.panel-caption{margin-top:10pt}.quarto-layout-panel>.table-caption{margin-top:0px}.table-caption p{margin-bottom:.5em}.quarto-layout-row{display:flex;flex-direction:row;align-items:flex-start}.quarto-layout-valign-top{align-items:flex-start}.quarto-layout-valign-bottom{align-items:flex-end}.quarto-layout-valign-center{align-items:center}.quarto-layout-cell{position:relative;margin-right:20px}.quarto-layout-cell:last-child{margin-right:0}.quarto-layout-cell figure,.quarto-layout-cell>p{margin:.2em}.quarto-layout-cell img{max-width:100%}.quarto-layout-cell .html-widget{width:100% !important}.quarto-layout-cell div figure p{margin:0}.quarto-layout-cell figure{display:block;margin-inline-start:0;margin-inline-end:0}.quarto-layout-cell table{display:inline-table}.quarto-layout-cell-subref figcaption,figure .quarto-layout-row figure figcaption{text-align:center;font-style:italic}.quarto-figure{position:relative;margin-bottom:1em}.quarto-figure>figure{width:100%;margin-bottom:0}.quarto-figure-left>figure>p,.quarto-figure-left>figure>div{text-align:left}.quarto-figure-center>figure>p,.quarto-figure-center>figure>div{text-align:center}.quarto-figure-right>figure>p,.quarto-figure-right>figure>div{text-align:right}.quarto-figure>figure>div.cell-annotation,.quarto-figure>figure>div code{text-align:left}figure>p:empty{display:none}figure>p:first-child{margin-top:0;margin-bottom:0}figure>figcaption.quarto-float-caption-bottom{margin-bottom:.5em}figure>figcaption.quarto-float-caption-top{margin-top:.5em}div[id^=tbl-]{position:relative}.quarto-figure>.anchorjs-link{position:absolute;top:.6em;right:.5em}div[id^=tbl-]>.anchorjs-link{position:absolute;top:.7em;right:.3em}.quarto-figure:hover>.anchorjs-link,div[id^=tbl-]:hover>.anchorjs-link,h2:hover>.anchorjs-link,.h2:hover>.anchorjs-link,h3:hover>.anchorjs-link,.h3:hover>.anchorjs-link,h4:hover>.anchorjs-link,.h4:hover>.anchorjs-link,h5:hover>.anchorjs-link,.h5:hover>.anchorjs-link,h6:hover>.anchorjs-link,.h6:hover>.anchorjs-link,.reveal-anchorjs-link>.anchorjs-link{opacity:1}#title-block-header{margin-block-end:1rem;position:relative;margin-top:-1px}#title-block-header .abstract{margin-block-start:1rem}#title-block-header .abstract .abstract-title{font-weight:600}#title-block-header a{text-decoration:none}#title-block-header .author,#title-block-header .date,#title-block-header .doi{margin-block-end:.2rem}#title-block-header .quarto-title-block>div{display:flex}#title-block-header .quarto-title-block>div>h1,#title-block-header .quarto-title-block>div>.h1{flex-grow:1}#title-block-header .quarto-title-block>div>button{flex-shrink:0;height:2.25rem;margin-top:0}@media(min-width: 992px){#title-block-header .quarto-title-block>div>button{margin-top:5px}}tr.header>th>p:last-of-type{margin-bottom:0px}table,table.table{margin-top:.5rem;margin-bottom:.5rem}caption,.table-caption{padding-top:.5rem;padding-bottom:.5rem;text-align:center}figure.quarto-float-tbl figcaption.quarto-float-caption-top{margin-top:.5rem;margin-bottom:.25rem;text-align:center}figure.quarto-float-tbl figcaption.quarto-float-caption-bottom{padding-top:.25rem;margin-bottom:.5rem;text-align:center}.utterances{max-width:none;margin-left:-8px}iframe{margin-bottom:1em}details{margin-bottom:1em}details[show]{margin-bottom:0}details>summary{color:#6c757d}details>summary>p:only-child{display:inline}pre.sourceCode,code.sourceCode{position:relative}dd code:not(.sourceCode),p code:not(.sourceCode){white-space:pre-wrap}code{white-space:pre}@media print{code{white-space:pre-wrap}}pre>code{display:block}pre>code.sourceCode{white-space:pre}pre>code.sourceCode>span>a:first-child::before{text-decoration:none}pre.code-overflow-wrap>code.sourceCode{white-space:pre-wrap}pre.code-overflow-scroll>code.sourceCode{white-space:pre}code a:any-link{color:inherit;text-decoration:none}code a:hover{color:inherit;text-decoration:underline}ul.task-list{padding-left:1em}[data-tippy-root]{display:inline-block}.tippy-content .footnote-back{display:none}.footnote-back{margin-left:.2em}.tippy-content{overflow-x:auto}.quarto-embedded-source-code{display:none}.quarto-unresolved-ref{font-weight:600}.quarto-cover-image{max-width:35%;float:right;margin-left:30px}.cell-output-display .widget-subarea{margin-bottom:1em}.cell-output-display:not(.no-overflow-x),.knitsql-table:not(.no-overflow-x){overflow-x:auto}.panel-input{margin-bottom:1em}.panel-input>div,.panel-input>div>div{display:inline-block;vertical-align:top;padding-right:12px}.panel-input>p:last-child{margin-bottom:0}.layout-sidebar{margin-bottom:1em}.layout-sidebar .tab-content{border:none}.tab-content>.page-columns.active{display:grid}div.sourceCode>iframe{width:100%;height:300px;margin-bottom:-0.5em}a{text-underline-offset:3px}div.ansi-escaped-output{font-family:monospace;display:block}/*!
+*
+* ansi colors from IPython notebook's
+*
+* we also add `bright-[color]-` synonyms for the `-[color]-intense` classes since
+* that seems to be what ansi_up emits
+*
+*/.ansi-black-fg{color:#3e424d}.ansi-black-bg{background-color:#3e424d}.ansi-black-intense-black,.ansi-bright-black-fg{color:#282c36}.ansi-black-intense-black,.ansi-bright-black-bg{background-color:#282c36}.ansi-red-fg{color:#e75c58}.ansi-red-bg{background-color:#e75c58}.ansi-red-intense-red,.ansi-bright-red-fg{color:#b22b31}.ansi-red-intense-red,.ansi-bright-red-bg{background-color:#b22b31}.ansi-green-fg{color:#00a250}.ansi-green-bg{background-color:#00a250}.ansi-green-intense-green,.ansi-bright-green-fg{color:#007427}.ansi-green-intense-green,.ansi-bright-green-bg{background-color:#007427}.ansi-yellow-fg{color:#ddb62b}.ansi-yellow-bg{background-color:#ddb62b}.ansi-yellow-intense-yellow,.ansi-bright-yellow-fg{color:#b27d12}.ansi-yellow-intense-yellow,.ansi-bright-yellow-bg{background-color:#b27d12}.ansi-blue-fg{color:#208ffb}.ansi-blue-bg{background-color:#208ffb}.ansi-blue-intense-blue,.ansi-bright-blue-fg{color:#0065ca}.ansi-blue-intense-blue,.ansi-bright-blue-bg{background-color:#0065ca}.ansi-magenta-fg{color:#d160c4}.ansi-magenta-bg{background-color:#d160c4}.ansi-magenta-intense-magenta,.ansi-bright-magenta-fg{color:#a03196}.ansi-magenta-intense-magenta,.ansi-bright-magenta-bg{background-color:#a03196}.ansi-cyan-fg{color:#60c6c8}.ansi-cyan-bg{background-color:#60c6c8}.ansi-cyan-intense-cyan,.ansi-bright-cyan-fg{color:#258f8f}.ansi-cyan-intense-cyan,.ansi-bright-cyan-bg{background-color:#258f8f}.ansi-white-fg{color:#c5c1b4}.ansi-white-bg{background-color:#c5c1b4}.ansi-white-intense-white,.ansi-bright-white-fg{color:#a1a6b2}.ansi-white-intense-white,.ansi-bright-white-bg{background-color:#a1a6b2}.ansi-default-inverse-fg{color:#fff}.ansi-default-inverse-bg{background-color:#000}.ansi-bold{font-weight:bold}.ansi-underline{text-decoration:underline}:root{--quarto-body-bg: #FFFFFF;--quarto-body-color: #000000;--quarto-text-muted: #6c757d;--quarto-border-color: #dee2e6;--quarto-border-width: 1px;--quarto-border-radius: 0.25rem}table.gt_table{color:var(--quarto-body-color);font-size:1em;width:100%;background-color:rgba(0,0,0,0);border-top-width:inherit;border-bottom-width:inherit;border-color:var(--quarto-border-color)}table.gt_table th.gt_column_spanner_outer{color:var(--quarto-body-color);background-color:rgba(0,0,0,0);border-top-width:inherit;border-bottom-width:inherit;border-color:var(--quarto-border-color)}table.gt_table th.gt_col_heading{color:var(--quarto-body-color);font-weight:bold;background-color:rgba(0,0,0,0)}table.gt_table thead.gt_col_headings{border-bottom:1px solid currentColor;border-top-width:inherit;border-top-color:var(--quarto-border-color)}table.gt_table thead.gt_col_headings:not(:first-child){border-top-width:1px;border-top-color:var(--quarto-border-color)}table.gt_table td.gt_row{border-bottom-width:1px;border-bottom-color:var(--quarto-border-color);border-top-width:0px}table.gt_table tbody.gt_table_body{border-top-width:1px;border-bottom-width:1px;border-bottom-color:var(--quarto-border-color);border-top-color:currentColor}div.columns{display:initial;gap:initial}div.column{display:inline-block;overflow-x:initial;vertical-align:top;width:50%}.code-annotation-tip-content{word-wrap:break-word}.code-annotation-container-hidden{display:none !important}dl.code-annotation-container-grid{display:grid;grid-template-columns:min-content auto}dl.code-annotation-container-grid dt{grid-column:1}dl.code-annotation-container-grid dd{grid-column:2}pre.sourceCode.code-annotation-code{padding-right:0}code.sourceCode .code-annotation-anchor{z-index:100;position:relative;float:right;background-color:rgba(0,0,0,0)}input[type=checkbox]{margin-right:.5ch}:root{--mermaid-bg-color: #FFFFFF;--mermaid-edge-color: #343a40;--mermaid-node-fg-color: #000000;--mermaid-fg-color: #000000;--mermaid-fg-color--lighter: #1a1a1a;--mermaid-fg-color--lightest: #333333;--mermaid-font-family: Source Sans Pro, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;--mermaid-label-bg-color: #FFFFFF;--mermaid-label-fg-color: #E8E8FC;--mermaid-node-bg-color: rgba(232, 232, 252, 0.1);--mermaid-node-fg-color: #000000}@media print{:root{font-size:11pt}#quarto-sidebar,#TOC,.nav-page{display:none}.page-columns .content{grid-column-start:page-start}.fixed-top{position:relative}.panel-caption,.figure-caption,figcaption{color:#666}}.code-copy-button{position:absolute;top:0;right:0;border:0;margin-top:5px;margin-right:5px;background-color:rgba(0,0,0,0);z-index:3}.code-copy-button:focus{outline:none}.code-copy-button-tooltip{font-size:.75em}pre.sourceCode:hover>.code-copy-button>.bi::before{display:inline-block;height:1rem;width:1rem;content:"";vertical-align:-0.125em;background-image:url('data:image/svg+xml,');background-repeat:no-repeat;background-size:1rem 1rem}pre.sourceCode:hover>.code-copy-button-checked>.bi::before{background-image:url('data:image/svg+xml,')}pre.sourceCode:hover>.code-copy-button:hover>.bi::before{background-image:url('data:image/svg+xml,')}pre.sourceCode:hover>.code-copy-button-checked:hover>.bi::before{background-image:url('data:image/svg+xml,')}main ol ol,main ul ul,main ol ul,main ul ol{margin-bottom:1em}ul>li:not(:has(>p))>ul,ol>li:not(:has(>p))>ul,ul>li:not(:has(>p))>ol,ol>li:not(:has(>p))>ol{margin-bottom:0}ul>li:not(:has(>p))>ul>li:has(>p),ol>li:not(:has(>p))>ul>li:has(>p),ul>li:not(:has(>p))>ol>li:has(>p),ol>li:not(:has(>p))>ol>li:has(>p){margin-top:1rem}body{margin:0}main.page-columns>header>h1.title,main.page-columns>header>.title.h1{margin-bottom:0}@media(min-width: 992px){body .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset] 35px [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(500px, calc(850px - 3em)) [body-content-end] 1.5em [body-end] 35px [body-end-outset] minmax(75px, 145px) [page-end-inset] 35px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.fullcontent:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset] 35px [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(500px, calc(850px - 3em)) [body-content-end] 1.5em [body-end] 35px [body-end-outset] 35px [page-end-inset page-end] 5fr [screen-end-inset] 1.5em}body.slimcontent:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset] 35px [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(500px, calc(850px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(0px, 200px) [page-end-inset] 35px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.listing:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start] minmax(50px, 100px) [page-start-inset] 50px [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(500px, calc(850px - 3em)) [body-content-end] 3em [body-end] 50px [body-end-outset] minmax(0px, 250px) [page-end-inset] minmax(50px, 100px) [page-end] 1fr [screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 35px [page-start-inset] minmax(0px, 175px) [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(450px, calc(800px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(0px, 200px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 35px [page-start-inset] minmax(0px, 175px) [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(450px, calc(800px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(0px, 200px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] minmax(25px, 50px) [page-start-inset] minmax(50px, 150px) [body-start-outset] minmax(25px, 50px) [body-start] 1.5em [body-content-start] minmax(500px, calc(800px - 3em)) [body-content-end] 1.5em [body-end] minmax(25px, 50px) [body-end-outset] minmax(50px, 150px) [page-end-inset] minmax(25px, 50px) [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start] minmax(50px, 100px) [page-start-inset] 50px [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(500px, calc(1000px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(50px, 100px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked.fullcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start] minmax(50px, 100px) [page-start-inset] 50px [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(500px, calc(1000px - 3em)) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating.fullcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 50px [page-start-inset] minmax(50px, 150px) [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(500px, calc(800px - 3em)) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked.slimcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start] minmax(50px, 100px) [page-start-inset] 50px [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(450px, calc(750px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(0px, 200px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked.listing .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start] minmax(50px, 100px) [page-start-inset] 50px [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(500px, calc(1000px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(0px, 200px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating.slimcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 50px [page-start-inset] minmax(50px, 150px) [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(450px, calc(750px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(50px, 150px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating.listing .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] minmax(25px, 50px) [page-start-inset] minmax(50px, 150px) [body-start-outset] minmax(25px, 50px) [body-start] 1.5em [body-content-start] minmax(500px, calc(800px - 3em)) [body-content-end] 1.5em [body-end] minmax(25px, 50px) [body-end-outset] minmax(50px, 150px) [page-end-inset] minmax(25px, 50px) [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}}@media(max-width: 991.98px){body .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset] 5fr [body-start] 1.5em [body-content-start] minmax(500px, calc(800px - 3em)) [body-content-end] 1.5em [body-end] 35px [body-end-outset] minmax(75px, 145px) [page-end-inset] 35px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.fullcontent:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset] 5fr [body-start] 1.5em [body-content-start] minmax(500px, calc(800px - 3em)) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.slimcontent:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset] 5fr [body-start] 1.5em [body-content-start] minmax(500px, calc(800px - 3em)) [body-content-end] 1.5em [body-end] 35px [body-end-outset] minmax(75px, 145px) [page-end-inset] 35px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.listing:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset] 5fr [body-start] 1.5em [body-content-start] minmax(500px, calc(1250px - 3em)) [body-content-end body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 35px [page-start-inset] minmax(0px, 145px) [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(450px, calc(800px - 3em)) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 35px [page-start-inset] minmax(0px, 145px) [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(450px, calc(800px - 3em)) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset body-start-outset body-start] 1.5em [body-content-start] minmax(500px, calc(750px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(75px, 150px) [page-end-inset] 25px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(500px, calc(750px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(25px, 50px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked.fullcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(500px, calc(1000px - 3em)) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating.fullcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset body-start-outset body-start] 1em [body-content-start] minmax(500px, calc(800px - 3em)) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 4fr [screen-end-inset] 1.5em [screen-end]}body.docked.slimcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(500px, calc(750px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(25px, 50px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked.listing .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(500px, calc(750px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(25px, 50px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating.slimcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset body-start-outset body-start] 1em [body-content-start] minmax(500px, calc(750px - 3em)) [body-content-end] 1.5em [body-end] 35px [body-end-outset] minmax(75px, 145px) [page-end-inset] 35px [page-end] 4fr [screen-end-inset] 1.5em [screen-end]}body.floating.listing .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset body-start-outset body-start] 1em [body-content-start] minmax(500px, calc(750px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(75px, 150px) [page-end-inset] 25px [page-end] 4fr [screen-end-inset] 1.5em [screen-end]}}@media(max-width: 767.98px){body .page-columns,body.fullcontent:not(.floating):not(.docked) .page-columns,body.slimcontent:not(.floating):not(.docked) .page-columns,body.docked .page-columns,body.docked.slimcontent .page-columns,body.docked.fullcontent .page-columns,body.floating .page-columns,body.floating.slimcontent .page-columns,body.floating.fullcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(0px, 1fr) [body-content-end body-end body-end-outset page-end-inset page-end screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(0px, 1fr) [body-content-end body-end body-end-outset page-end-inset page-end screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(0px, 1fr) [body-content-end body-end body-end-outset page-end-inset page-end screen-end-inset] 1.5em [screen-end]}nav[role=doc-toc]{display:none}}body,.page-row-navigation{grid-template-rows:[page-top] max-content [contents-top] max-content [contents-bottom] max-content [page-bottom]}.page-rows-contents{grid-template-rows:[content-top] minmax(max-content, 1fr) [content-bottom] minmax(60px, max-content) [page-bottom]}.page-full{grid-column:screen-start/screen-end !important}.page-columns>*{grid-column:body-content-start/body-content-end}.page-columns.column-page>*{grid-column:page-start/page-end}.page-columns.column-page-left .page-columns.page-full>*,.page-columns.column-page-left>*{grid-column:page-start/body-content-end}.page-columns.column-page-right .page-columns.page-full>*,.page-columns.column-page-right>*{grid-column:body-content-start/page-end}.page-rows{grid-auto-rows:auto}.header{grid-column:screen-start/screen-end;grid-row:page-top/contents-top}#quarto-content{padding:0;grid-column:screen-start/screen-end;grid-row:contents-top/contents-bottom}body.floating .sidebar.sidebar-navigation{grid-column:page-start/body-start;grid-row:content-top/page-bottom}body.docked .sidebar.sidebar-navigation{grid-column:screen-start/body-start;grid-row:content-top/page-bottom}.sidebar.toc-left{grid-column:page-start/body-start;grid-row:content-top/page-bottom}.sidebar.margin-sidebar{grid-column:body-end/page-end;grid-row:content-top/page-bottom}.page-columns .content{grid-column:body-content-start/body-content-end;grid-row:content-top/content-bottom;align-content:flex-start}.page-columns .page-navigation{grid-column:body-content-start/body-content-end;grid-row:content-bottom/page-bottom}.page-columns .footer{grid-column:screen-start/screen-end;grid-row:contents-bottom/page-bottom}.page-columns .column-body{grid-column:body-content-start/body-content-end}.page-columns .column-body-fullbleed{grid-column:body-start/body-end}.page-columns .column-body-outset{grid-column:body-start-outset/body-end-outset;z-index:998;opacity:.999}.page-columns .column-body-outset table{background:#fff}.page-columns .column-body-outset-left{grid-column:body-start-outset/body-content-end;z-index:998;opacity:.999}.page-columns .column-body-outset-left table{background:#fff}.page-columns .column-body-outset-right{grid-column:body-content-start/body-end-outset;z-index:998;opacity:.999}.page-columns .column-body-outset-right table{background:#fff}.page-columns .column-page{grid-column:page-start/page-end;z-index:998;opacity:.999}.page-columns .column-page table{background:#fff}.page-columns .column-page-inset{grid-column:page-start-inset/page-end-inset;z-index:998;opacity:.999}.page-columns .column-page-inset table{background:#fff}.page-columns .column-page-inset-left{grid-column:page-start-inset/body-content-end;z-index:998;opacity:.999}.page-columns .column-page-inset-left table{background:#fff}.page-columns .column-page-inset-right{grid-column:body-content-start/page-end-inset;z-index:998;opacity:.999}.page-columns .column-page-inset-right figcaption table{background:#fff}.page-columns .column-page-left{grid-column:page-start/body-content-end;z-index:998;opacity:.999}.page-columns .column-page-left table{background:#fff}.page-columns .column-page-right{grid-column:body-content-start/page-end;z-index:998;opacity:.999}.page-columns .column-page-right figcaption table{background:#fff}#quarto-content.page-columns #quarto-margin-sidebar,#quarto-content.page-columns #quarto-sidebar{z-index:1}@media(max-width: 991.98px){#quarto-content.page-columns #quarto-margin-sidebar.collapse,#quarto-content.page-columns #quarto-sidebar.collapse,#quarto-content.page-columns #quarto-margin-sidebar.collapsing,#quarto-content.page-columns #quarto-sidebar.collapsing{z-index:1055}}#quarto-content.page-columns main.column-page,#quarto-content.page-columns main.column-page-right,#quarto-content.page-columns main.column-page-left{z-index:0}.page-columns .column-screen-inset{grid-column:screen-start-inset/screen-end-inset;z-index:998;opacity:.999}.page-columns .column-screen-inset table{background:#fff}.page-columns .column-screen-inset-left{grid-column:screen-start-inset/body-content-end;z-index:998;opacity:.999}.page-columns .column-screen-inset-left table{background:#fff}.page-columns .column-screen-inset-right{grid-column:body-content-start/screen-end-inset;z-index:998;opacity:.999}.page-columns .column-screen-inset-right table{background:#fff}.page-columns .column-screen{grid-column:screen-start/screen-end;z-index:998;opacity:.999}.page-columns .column-screen table{background:#fff}.page-columns .column-screen-left{grid-column:screen-start/body-content-end;z-index:998;opacity:.999}.page-columns .column-screen-left table{background:#fff}.page-columns .column-screen-right{grid-column:body-content-start/screen-end;z-index:998;opacity:.999}.page-columns .column-screen-right table{background:#fff}.page-columns .column-screen-inset-shaded{grid-column:screen-start/screen-end;padding:1em;background:#f8f9fa;z-index:998;opacity:.999;margin-bottom:1em}.zindex-content{z-index:998;opacity:.999}.zindex-modal{z-index:1055;opacity:.999}.zindex-over-content{z-index:999;opacity:.999}img.img-fluid.column-screen,img.img-fluid.column-screen-inset-shaded,img.img-fluid.column-screen-inset,img.img-fluid.column-screen-inset-left,img.img-fluid.column-screen-inset-right,img.img-fluid.column-screen-left,img.img-fluid.column-screen-right{width:100%}@media(min-width: 992px){.margin-caption,div.aside,aside:not(.footnotes):not(.sidebar),.column-margin{grid-column:body-end/page-end !important;z-index:998}.column-sidebar{grid-column:page-start/body-start !important;z-index:998}.column-leftmargin{grid-column:screen-start-inset/body-start !important;z-index:998}.no-row-height{height:1em;overflow:visible}}@media(max-width: 991.98px){.margin-caption,div.aside,aside:not(.footnotes):not(.sidebar),.column-margin{grid-column:body-end/page-end !important;z-index:998}.no-row-height{height:1em;overflow:visible}.page-columns.page-full{overflow:visible}.page-columns.toc-left .margin-caption,.page-columns.toc-left div.aside,.page-columns.toc-left aside:not(.footnotes):not(.sidebar),.page-columns.toc-left .column-margin{grid-column:body-content-start/body-content-end !important;z-index:998;opacity:.999}.page-columns.toc-left .no-row-height{height:initial;overflow:initial}}@media(max-width: 767.98px){.margin-caption,div.aside,aside:not(.footnotes):not(.sidebar),.column-margin{grid-column:body-content-start/body-content-end !important;z-index:998;opacity:.999}.no-row-height{height:initial;overflow:initial}#quarto-margin-sidebar{display:none}#quarto-sidebar-toc-left{display:none}.hidden-sm{display:none}}.panel-grid{display:grid;grid-template-rows:repeat(1, 1fr);grid-template-columns:repeat(24, 1fr);gap:1em}.panel-grid .g-col-1{grid-column:auto/span 1}.panel-grid .g-col-2{grid-column:auto/span 2}.panel-grid .g-col-3{grid-column:auto/span 3}.panel-grid .g-col-4{grid-column:auto/span 4}.panel-grid .g-col-5{grid-column:auto/span 5}.panel-grid .g-col-6{grid-column:auto/span 6}.panel-grid .g-col-7{grid-column:auto/span 7}.panel-grid .g-col-8{grid-column:auto/span 8}.panel-grid .g-col-9{grid-column:auto/span 9}.panel-grid .g-col-10{grid-column:auto/span 10}.panel-grid .g-col-11{grid-column:auto/span 11}.panel-grid .g-col-12{grid-column:auto/span 12}.panel-grid .g-col-13{grid-column:auto/span 13}.panel-grid .g-col-14{grid-column:auto/span 14}.panel-grid .g-col-15{grid-column:auto/span 15}.panel-grid .g-col-16{grid-column:auto/span 16}.panel-grid .g-col-17{grid-column:auto/span 17}.panel-grid .g-col-18{grid-column:auto/span 18}.panel-grid .g-col-19{grid-column:auto/span 19}.panel-grid .g-col-20{grid-column:auto/span 20}.panel-grid .g-col-21{grid-column:auto/span 21}.panel-grid .g-col-22{grid-column:auto/span 22}.panel-grid .g-col-23{grid-column:auto/span 23}.panel-grid .g-col-24{grid-column:auto/span 24}.panel-grid .g-start-1{grid-column-start:1}.panel-grid .g-start-2{grid-column-start:2}.panel-grid .g-start-3{grid-column-start:3}.panel-grid .g-start-4{grid-column-start:4}.panel-grid .g-start-5{grid-column-start:5}.panel-grid .g-start-6{grid-column-start:6}.panel-grid .g-start-7{grid-column-start:7}.panel-grid .g-start-8{grid-column-start:8}.panel-grid .g-start-9{grid-column-start:9}.panel-grid .g-start-10{grid-column-start:10}.panel-grid .g-start-11{grid-column-start:11}.panel-grid .g-start-12{grid-column-start:12}.panel-grid .g-start-13{grid-column-start:13}.panel-grid .g-start-14{grid-column-start:14}.panel-grid .g-start-15{grid-column-start:15}.panel-grid .g-start-16{grid-column-start:16}.panel-grid .g-start-17{grid-column-start:17}.panel-grid .g-start-18{grid-column-start:18}.panel-grid .g-start-19{grid-column-start:19}.panel-grid .g-start-20{grid-column-start:20}.panel-grid .g-start-21{grid-column-start:21}.panel-grid .g-start-22{grid-column-start:22}.panel-grid .g-start-23{grid-column-start:23}@media(min-width: 576px){.panel-grid .g-col-sm-1{grid-column:auto/span 1}.panel-grid .g-col-sm-2{grid-column:auto/span 2}.panel-grid .g-col-sm-3{grid-column:auto/span 3}.panel-grid .g-col-sm-4{grid-column:auto/span 4}.panel-grid .g-col-sm-5{grid-column:auto/span 5}.panel-grid .g-col-sm-6{grid-column:auto/span 6}.panel-grid .g-col-sm-7{grid-column:auto/span 7}.panel-grid .g-col-sm-8{grid-column:auto/span 8}.panel-grid .g-col-sm-9{grid-column:auto/span 9}.panel-grid .g-col-sm-10{grid-column:auto/span 10}.panel-grid .g-col-sm-11{grid-column:auto/span 11}.panel-grid .g-col-sm-12{grid-column:auto/span 12}.panel-grid .g-col-sm-13{grid-column:auto/span 13}.panel-grid .g-col-sm-14{grid-column:auto/span 14}.panel-grid .g-col-sm-15{grid-column:auto/span 15}.panel-grid .g-col-sm-16{grid-column:auto/span 16}.panel-grid .g-col-sm-17{grid-column:auto/span 17}.panel-grid .g-col-sm-18{grid-column:auto/span 18}.panel-grid .g-col-sm-19{grid-column:auto/span 19}.panel-grid .g-col-sm-20{grid-column:auto/span 20}.panel-grid .g-col-sm-21{grid-column:auto/span 21}.panel-grid .g-col-sm-22{grid-column:auto/span 22}.panel-grid .g-col-sm-23{grid-column:auto/span 23}.panel-grid .g-col-sm-24{grid-column:auto/span 24}.panel-grid .g-start-sm-1{grid-column-start:1}.panel-grid .g-start-sm-2{grid-column-start:2}.panel-grid .g-start-sm-3{grid-column-start:3}.panel-grid .g-start-sm-4{grid-column-start:4}.panel-grid .g-start-sm-5{grid-column-start:5}.panel-grid .g-start-sm-6{grid-column-start:6}.panel-grid .g-start-sm-7{grid-column-start:7}.panel-grid .g-start-sm-8{grid-column-start:8}.panel-grid .g-start-sm-9{grid-column-start:9}.panel-grid .g-start-sm-10{grid-column-start:10}.panel-grid .g-start-sm-11{grid-column-start:11}.panel-grid .g-start-sm-12{grid-column-start:12}.panel-grid .g-start-sm-13{grid-column-start:13}.panel-grid .g-start-sm-14{grid-column-start:14}.panel-grid .g-start-sm-15{grid-column-start:15}.panel-grid .g-start-sm-16{grid-column-start:16}.panel-grid .g-start-sm-17{grid-column-start:17}.panel-grid .g-start-sm-18{grid-column-start:18}.panel-grid .g-start-sm-19{grid-column-start:19}.panel-grid .g-start-sm-20{grid-column-start:20}.panel-grid .g-start-sm-21{grid-column-start:21}.panel-grid .g-start-sm-22{grid-column-start:22}.panel-grid .g-start-sm-23{grid-column-start:23}}@media(min-width: 768px){.panel-grid .g-col-md-1{grid-column:auto/span 1}.panel-grid .g-col-md-2{grid-column:auto/span 2}.panel-grid .g-col-md-3{grid-column:auto/span 3}.panel-grid .g-col-md-4{grid-column:auto/span 4}.panel-grid .g-col-md-5{grid-column:auto/span 5}.panel-grid .g-col-md-6{grid-column:auto/span 6}.panel-grid .g-col-md-7{grid-column:auto/span 7}.panel-grid .g-col-md-8{grid-column:auto/span 8}.panel-grid .g-col-md-9{grid-column:auto/span 9}.panel-grid .g-col-md-10{grid-column:auto/span 10}.panel-grid .g-col-md-11{grid-column:auto/span 11}.panel-grid .g-col-md-12{grid-column:auto/span 12}.panel-grid .g-col-md-13{grid-column:auto/span 13}.panel-grid .g-col-md-14{grid-column:auto/span 14}.panel-grid .g-col-md-15{grid-column:auto/span 15}.panel-grid .g-col-md-16{grid-column:auto/span 16}.panel-grid .g-col-md-17{grid-column:auto/span 17}.panel-grid .g-col-md-18{grid-column:auto/span 18}.panel-grid .g-col-md-19{grid-column:auto/span 19}.panel-grid .g-col-md-20{grid-column:auto/span 20}.panel-grid .g-col-md-21{grid-column:auto/span 21}.panel-grid .g-col-md-22{grid-column:auto/span 22}.panel-grid .g-col-md-23{grid-column:auto/span 23}.panel-grid .g-col-md-24{grid-column:auto/span 24}.panel-grid .g-start-md-1{grid-column-start:1}.panel-grid .g-start-md-2{grid-column-start:2}.panel-grid .g-start-md-3{grid-column-start:3}.panel-grid .g-start-md-4{grid-column-start:4}.panel-grid .g-start-md-5{grid-column-start:5}.panel-grid .g-start-md-6{grid-column-start:6}.panel-grid .g-start-md-7{grid-column-start:7}.panel-grid .g-start-md-8{grid-column-start:8}.panel-grid .g-start-md-9{grid-column-start:9}.panel-grid .g-start-md-10{grid-column-start:10}.panel-grid .g-start-md-11{grid-column-start:11}.panel-grid .g-start-md-12{grid-column-start:12}.panel-grid .g-start-md-13{grid-column-start:13}.panel-grid .g-start-md-14{grid-column-start:14}.panel-grid .g-start-md-15{grid-column-start:15}.panel-grid .g-start-md-16{grid-column-start:16}.panel-grid .g-start-md-17{grid-column-start:17}.panel-grid .g-start-md-18{grid-column-start:18}.panel-grid .g-start-md-19{grid-column-start:19}.panel-grid .g-start-md-20{grid-column-start:20}.panel-grid .g-start-md-21{grid-column-start:21}.panel-grid .g-start-md-22{grid-column-start:22}.panel-grid .g-start-md-23{grid-column-start:23}}@media(min-width: 992px){.panel-grid .g-col-lg-1{grid-column:auto/span 1}.panel-grid .g-col-lg-2{grid-column:auto/span 2}.panel-grid .g-col-lg-3{grid-column:auto/span 3}.panel-grid .g-col-lg-4{grid-column:auto/span 4}.panel-grid .g-col-lg-5{grid-column:auto/span 5}.panel-grid .g-col-lg-6{grid-column:auto/span 6}.panel-grid .g-col-lg-7{grid-column:auto/span 7}.panel-grid .g-col-lg-8{grid-column:auto/span 8}.panel-grid .g-col-lg-9{grid-column:auto/span 9}.panel-grid .g-col-lg-10{grid-column:auto/span 10}.panel-grid .g-col-lg-11{grid-column:auto/span 11}.panel-grid .g-col-lg-12{grid-column:auto/span 12}.panel-grid .g-col-lg-13{grid-column:auto/span 13}.panel-grid .g-col-lg-14{grid-column:auto/span 14}.panel-grid .g-col-lg-15{grid-column:auto/span 15}.panel-grid .g-col-lg-16{grid-column:auto/span 16}.panel-grid .g-col-lg-17{grid-column:auto/span 17}.panel-grid .g-col-lg-18{grid-column:auto/span 18}.panel-grid .g-col-lg-19{grid-column:auto/span 19}.panel-grid .g-col-lg-20{grid-column:auto/span 20}.panel-grid .g-col-lg-21{grid-column:auto/span 21}.panel-grid .g-col-lg-22{grid-column:auto/span 22}.panel-grid .g-col-lg-23{grid-column:auto/span 23}.panel-grid .g-col-lg-24{grid-column:auto/span 24}.panel-grid .g-start-lg-1{grid-column-start:1}.panel-grid .g-start-lg-2{grid-column-start:2}.panel-grid .g-start-lg-3{grid-column-start:3}.panel-grid .g-start-lg-4{grid-column-start:4}.panel-grid .g-start-lg-5{grid-column-start:5}.panel-grid .g-start-lg-6{grid-column-start:6}.panel-grid .g-start-lg-7{grid-column-start:7}.panel-grid .g-start-lg-8{grid-column-start:8}.panel-grid .g-start-lg-9{grid-column-start:9}.panel-grid .g-start-lg-10{grid-column-start:10}.panel-grid .g-start-lg-11{grid-column-start:11}.panel-grid .g-start-lg-12{grid-column-start:12}.panel-grid .g-start-lg-13{grid-column-start:13}.panel-grid .g-start-lg-14{grid-column-start:14}.panel-grid .g-start-lg-15{grid-column-start:15}.panel-grid .g-start-lg-16{grid-column-start:16}.panel-grid .g-start-lg-17{grid-column-start:17}.panel-grid .g-start-lg-18{grid-column-start:18}.panel-grid .g-start-lg-19{grid-column-start:19}.panel-grid .g-start-lg-20{grid-column-start:20}.panel-grid .g-start-lg-21{grid-column-start:21}.panel-grid .g-start-lg-22{grid-column-start:22}.panel-grid .g-start-lg-23{grid-column-start:23}}@media(min-width: 1200px){.panel-grid .g-col-xl-1{grid-column:auto/span 1}.panel-grid .g-col-xl-2{grid-column:auto/span 2}.panel-grid .g-col-xl-3{grid-column:auto/span 3}.panel-grid .g-col-xl-4{grid-column:auto/span 4}.panel-grid .g-col-xl-5{grid-column:auto/span 5}.panel-grid .g-col-xl-6{grid-column:auto/span 6}.panel-grid .g-col-xl-7{grid-column:auto/span 7}.panel-grid .g-col-xl-8{grid-column:auto/span 8}.panel-grid .g-col-xl-9{grid-column:auto/span 9}.panel-grid .g-col-xl-10{grid-column:auto/span 10}.panel-grid .g-col-xl-11{grid-column:auto/span 11}.panel-grid .g-col-xl-12{grid-column:auto/span 12}.panel-grid .g-col-xl-13{grid-column:auto/span 13}.panel-grid .g-col-xl-14{grid-column:auto/span 14}.panel-grid .g-col-xl-15{grid-column:auto/span 15}.panel-grid .g-col-xl-16{grid-column:auto/span 16}.panel-grid .g-col-xl-17{grid-column:auto/span 17}.panel-grid .g-col-xl-18{grid-column:auto/span 18}.panel-grid .g-col-xl-19{grid-column:auto/span 19}.panel-grid .g-col-xl-20{grid-column:auto/span 20}.panel-grid .g-col-xl-21{grid-column:auto/span 21}.panel-grid .g-col-xl-22{grid-column:auto/span 22}.panel-grid .g-col-xl-23{grid-column:auto/span 23}.panel-grid .g-col-xl-24{grid-column:auto/span 24}.panel-grid .g-start-xl-1{grid-column-start:1}.panel-grid .g-start-xl-2{grid-column-start:2}.panel-grid .g-start-xl-3{grid-column-start:3}.panel-grid .g-start-xl-4{grid-column-start:4}.panel-grid .g-start-xl-5{grid-column-start:5}.panel-grid .g-start-xl-6{grid-column-start:6}.panel-grid .g-start-xl-7{grid-column-start:7}.panel-grid .g-start-xl-8{grid-column-start:8}.panel-grid .g-start-xl-9{grid-column-start:9}.panel-grid .g-start-xl-10{grid-column-start:10}.panel-grid .g-start-xl-11{grid-column-start:11}.panel-grid .g-start-xl-12{grid-column-start:12}.panel-grid .g-start-xl-13{grid-column-start:13}.panel-grid .g-start-xl-14{grid-column-start:14}.panel-grid .g-start-xl-15{grid-column-start:15}.panel-grid .g-start-xl-16{grid-column-start:16}.panel-grid .g-start-xl-17{grid-column-start:17}.panel-grid .g-start-xl-18{grid-column-start:18}.panel-grid .g-start-xl-19{grid-column-start:19}.panel-grid .g-start-xl-20{grid-column-start:20}.panel-grid .g-start-xl-21{grid-column-start:21}.panel-grid .g-start-xl-22{grid-column-start:22}.panel-grid .g-start-xl-23{grid-column-start:23}}@media(min-width: 1400px){.panel-grid .g-col-xxl-1{grid-column:auto/span 1}.panel-grid .g-col-xxl-2{grid-column:auto/span 2}.panel-grid .g-col-xxl-3{grid-column:auto/span 3}.panel-grid .g-col-xxl-4{grid-column:auto/span 4}.panel-grid .g-col-xxl-5{grid-column:auto/span 5}.panel-grid .g-col-xxl-6{grid-column:auto/span 6}.panel-grid .g-col-xxl-7{grid-column:auto/span 7}.panel-grid .g-col-xxl-8{grid-column:auto/span 8}.panel-grid .g-col-xxl-9{grid-column:auto/span 9}.panel-grid .g-col-xxl-10{grid-column:auto/span 10}.panel-grid .g-col-xxl-11{grid-column:auto/span 11}.panel-grid .g-col-xxl-12{grid-column:auto/span 12}.panel-grid .g-col-xxl-13{grid-column:auto/span 13}.panel-grid .g-col-xxl-14{grid-column:auto/span 14}.panel-grid .g-col-xxl-15{grid-column:auto/span 15}.panel-grid .g-col-xxl-16{grid-column:auto/span 16}.panel-grid .g-col-xxl-17{grid-column:auto/span 17}.panel-grid .g-col-xxl-18{grid-column:auto/span 18}.panel-grid .g-col-xxl-19{grid-column:auto/span 19}.panel-grid .g-col-xxl-20{grid-column:auto/span 20}.panel-grid .g-col-xxl-21{grid-column:auto/span 21}.panel-grid .g-col-xxl-22{grid-column:auto/span 22}.panel-grid .g-col-xxl-23{grid-column:auto/span 23}.panel-grid .g-col-xxl-24{grid-column:auto/span 24}.panel-grid .g-start-xxl-1{grid-column-start:1}.panel-grid .g-start-xxl-2{grid-column-start:2}.panel-grid .g-start-xxl-3{grid-column-start:3}.panel-grid .g-start-xxl-4{grid-column-start:4}.panel-grid .g-start-xxl-5{grid-column-start:5}.panel-grid .g-start-xxl-6{grid-column-start:6}.panel-grid .g-start-xxl-7{grid-column-start:7}.panel-grid .g-start-xxl-8{grid-column-start:8}.panel-grid .g-start-xxl-9{grid-column-start:9}.panel-grid .g-start-xxl-10{grid-column-start:10}.panel-grid .g-start-xxl-11{grid-column-start:11}.panel-grid .g-start-xxl-12{grid-column-start:12}.panel-grid .g-start-xxl-13{grid-column-start:13}.panel-grid .g-start-xxl-14{grid-column-start:14}.panel-grid .g-start-xxl-15{grid-column-start:15}.panel-grid .g-start-xxl-16{grid-column-start:16}.panel-grid .g-start-xxl-17{grid-column-start:17}.panel-grid .g-start-xxl-18{grid-column-start:18}.panel-grid .g-start-xxl-19{grid-column-start:19}.panel-grid .g-start-xxl-20{grid-column-start:20}.panel-grid .g-start-xxl-21{grid-column-start:21}.panel-grid .g-start-xxl-22{grid-column-start:22}.panel-grid .g-start-xxl-23{grid-column-start:23}}main{margin-top:1em;margin-bottom:1em}h1,.h1,h2,.h2{color:inherit;margin-top:2rem;margin-bottom:1rem;font-weight:600}h1.title,.title.h1{margin-top:0}main.content>section:first-of-type>h2:first-child,main.content>section:first-of-type>.h2:first-child{margin-top:0}h2,.h2{border-bottom:1px solid #dee2e6;padding-bottom:.5rem}h3,.h3{font-weight:600}h3,.h3,h4,.h4{opacity:.9;margin-top:1.5rem}h5,.h5,h6,.h6{opacity:.9}.header-section-number{color:#404040}.nav-link.active .header-section-number{color:inherit}mark,.mark{padding:0em}.panel-caption,.figure-caption,.subfigure-caption,.table-caption,figcaption,caption{font-size:.9rem;color:#404040}.quarto-layout-cell[data-ref-parent] caption{color:#404040}.column-margin figcaption,.margin-caption,div.aside,aside,.column-margin{color:#404040;font-size:.825rem}.panel-caption.margin-caption{text-align:inherit}.column-margin.column-container p{margin-bottom:0}.column-margin.column-container>*:not(.collapse):first-child{padding-bottom:.5em;display:block}.column-margin.column-container>*:not(.collapse):not(:first-child){padding-top:.5em;padding-bottom:.5em;display:block}.column-margin.column-container>*.collapse:not(.show){display:none}@media(min-width: 768px){.column-margin.column-container .callout-margin-content:first-child{margin-top:4.5em}.column-margin.column-container .callout-margin-content-simple:first-child{margin-top:3.5em}}.margin-caption>*{padding-top:.5em;padding-bottom:.5em}@media(max-width: 767.98px){.quarto-layout-row{flex-direction:column}}.nav-tabs .nav-item{margin-top:1px;cursor:pointer}.tab-content{margin-top:0px;border-left:#dee2e6 1px solid;border-right:#dee2e6 1px solid;border-bottom:#dee2e6 1px solid;margin-left:0;padding:1em;margin-bottom:1em}@media(max-width: 767.98px){.layout-sidebar{margin-left:0;margin-right:0}}.panel-sidebar,.panel-sidebar .form-control,.panel-input,.panel-input .form-control,.selectize-dropdown{font-size:.9rem}.panel-sidebar .form-control,.panel-input .form-control{padding-top:.1rem}.tab-pane div.sourceCode{margin-top:0px}.tab-pane>p{padding-top:0}.tab-pane>p:nth-child(1){padding-top:0}.tab-pane>p:last-child{margin-bottom:0}.tab-pane>pre:last-child{margin-bottom:0}.tab-content>.tab-pane:not(.active){display:none !important}div.sourceCode{background-color:rgba(233,236,239,.65);border:1px solid rgba(233,236,239,.65);border-radius:.25rem}pre.sourceCode{background-color:rgba(0,0,0,0)}pre.sourceCode{border:none;font-size:.875em;overflow:visible !important;padding:.4em}.callout pre.sourceCode{padding-left:0}div.sourceCode{overflow-y:hidden}.callout div.sourceCode{margin-left:initial}.blockquote{font-size:inherit;padding-left:1rem;padding-right:1.5rem;color:#404040}.blockquote h1:first-child,.blockquote .h1:first-child,.blockquote h2:first-child,.blockquote .h2:first-child,.blockquote h3:first-child,.blockquote .h3:first-child,.blockquote h4:first-child,.blockquote .h4:first-child,.blockquote h5:first-child,.blockquote .h5:first-child{margin-top:0}pre{background-color:initial;padding:initial;border:initial}p pre code:not(.sourceCode),li pre code:not(.sourceCode),pre code:not(.sourceCode){background-color:initial}p code:not(.sourceCode),li code:not(.sourceCode),td code:not(.sourceCode){background-color:#f8f9fa;padding:.2em}nav p code:not(.sourceCode),nav li code:not(.sourceCode),nav td code:not(.sourceCode){background-color:rgba(0,0,0,0);padding:0}td code:not(.sourceCode){white-space:pre-wrap}#quarto-embedded-source-code-modal>.modal-dialog{max-width:1000px;padding-left:1.75rem;padding-right:1.75rem}#quarto-embedded-source-code-modal>.modal-dialog>.modal-content>.modal-body{padding:0}#quarto-embedded-source-code-modal>.modal-dialog>.modal-content>.modal-body div.sourceCode{margin:0;padding:.2rem .2rem;border-radius:0px;border:none}#quarto-embedded-source-code-modal>.modal-dialog>.modal-content>.modal-header{padding:.7rem}.code-tools-button{font-size:1rem;padding:.15rem .15rem;margin-left:5px;color:#6c757d;background-color:rgba(0,0,0,0);transition:initial;cursor:pointer}.code-tools-button>.bi::before{display:inline-block;height:1rem;width:1rem;content:"";vertical-align:-0.125em;background-image:url('data:image/svg+xml,');background-repeat:no-repeat;background-size:1rem 1rem}.code-tools-button:hover>.bi::before{background-image:url('data:image/svg+xml,')}#quarto-embedded-source-code-modal .code-copy-button>.bi::before{background-image:url('data:image/svg+xml,')}#quarto-embedded-source-code-modal .code-copy-button-checked>.bi::before{background-image:url('data:image/svg+xml,')}.sidebar{will-change:top;transition:top 200ms linear;position:sticky;overflow-y:auto;padding-top:1.2em;max-height:100vh}.sidebar.toc-left,.sidebar.margin-sidebar{top:0px;padding-top:1em}.sidebar.quarto-banner-title-block-sidebar>*{padding-top:1.65em}figure .quarto-notebook-link{margin-top:.5em}.quarto-notebook-link{font-size:.75em;color:#6c757d;margin-bottom:1em;text-decoration:none;display:block}.quarto-notebook-link:hover{text-decoration:underline;color:#4040bf}.quarto-notebook-link::before{display:inline-block;height:.75rem;width:.75rem;margin-bottom:0em;margin-right:.25em;content:"";vertical-align:-0.125em;background-image:url('data:image/svg+xml,');background-repeat:no-repeat;background-size:.75rem .75rem}.toc-actions i.bi,.quarto-code-links i.bi,.quarto-other-links i.bi,.quarto-alternate-notebooks i.bi,.quarto-alternate-formats i.bi{margin-right:.4em;font-size:.8rem}.quarto-other-links-text-target .quarto-code-links i.bi,.quarto-other-links-text-target .quarto-other-links i.bi{margin-right:.2em}.quarto-other-formats-text-target .quarto-alternate-formats i.bi{margin-right:.1em}.toc-actions i.bi.empty,.quarto-code-links i.bi.empty,.quarto-other-links i.bi.empty,.quarto-alternate-notebooks i.bi.empty,.quarto-alternate-formats i.bi.empty{padding-left:1em}.quarto-notebook h2,.quarto-notebook .h2{border-bottom:none}.quarto-notebook .cell-container{display:flex}.quarto-notebook .cell-container .cell{flex-grow:4}.quarto-notebook .cell-container .cell-decorator{padding-top:1.5em;padding-right:1em;text-align:right}.quarto-notebook .cell-container.code-fold .cell-decorator{padding-top:3em}.quarto-notebook .cell-code code{white-space:pre-wrap}.quarto-notebook .cell .cell-output-stderr pre code,.quarto-notebook .cell .cell-output-stdout pre code{white-space:pre-wrap;overflow-wrap:anywhere}.toc-actions,.quarto-alternate-formats,.quarto-other-links,.quarto-code-links,.quarto-alternate-notebooks{padding-left:0em}.sidebar .toc-actions a,.sidebar .quarto-alternate-formats a,.sidebar .quarto-other-links a,.sidebar .quarto-code-links a,.sidebar .quarto-alternate-notebooks a,.sidebar nav[role=doc-toc] a{text-decoration:none}.sidebar .toc-actions a:hover,.sidebar .quarto-other-links a:hover,.sidebar .quarto-code-links a:hover,.sidebar .quarto-alternate-formats a:hover,.sidebar .quarto-alternate-notebooks a:hover{color:#4040bf}.sidebar .toc-actions h2,.sidebar .toc-actions .h2,.sidebar .quarto-code-links h2,.sidebar .quarto-code-links .h2,.sidebar .quarto-other-links h2,.sidebar .quarto-other-links .h2,.sidebar .quarto-alternate-notebooks h2,.sidebar .quarto-alternate-notebooks .h2,.sidebar .quarto-alternate-formats h2,.sidebar .quarto-alternate-formats .h2,.sidebar nav[role=doc-toc]>h2,.sidebar nav[role=doc-toc]>.h2{font-weight:500;margin-bottom:.2rem;margin-top:.3rem;font-family:inherit;border-bottom:0;padding-bottom:0;padding-top:0px}.sidebar .toc-actions>h2,.sidebar .toc-actions>.h2,.sidebar .quarto-code-links>h2,.sidebar .quarto-code-links>.h2,.sidebar .quarto-other-links>h2,.sidebar .quarto-other-links>.h2,.sidebar .quarto-alternate-notebooks>h2,.sidebar .quarto-alternate-notebooks>.h2,.sidebar .quarto-alternate-formats>h2,.sidebar .quarto-alternate-formats>.h2{font-size:.8rem}.sidebar nav[role=doc-toc]>h2,.sidebar nav[role=doc-toc]>.h2{font-size:.875rem}.sidebar nav[role=doc-toc]>ul a{border-left:1px solid #e9ecef;padding-left:.6rem}.sidebar .toc-actions h2>ul a,.sidebar .toc-actions .h2>ul a,.sidebar .quarto-code-links h2>ul a,.sidebar .quarto-code-links .h2>ul a,.sidebar .quarto-other-links h2>ul a,.sidebar .quarto-other-links .h2>ul a,.sidebar .quarto-alternate-notebooks h2>ul a,.sidebar .quarto-alternate-notebooks .h2>ul a,.sidebar .quarto-alternate-formats h2>ul a,.sidebar .quarto-alternate-formats .h2>ul a{border-left:none;padding-left:.6rem}.sidebar .toc-actions ul a:empty,.sidebar .quarto-code-links ul a:empty,.sidebar .quarto-other-links ul a:empty,.sidebar .quarto-alternate-notebooks ul a:empty,.sidebar .quarto-alternate-formats ul a:empty,.sidebar nav[role=doc-toc]>ul a:empty{display:none}.sidebar .toc-actions ul,.sidebar .quarto-code-links ul,.sidebar .quarto-other-links ul,.sidebar .quarto-alternate-notebooks ul,.sidebar .quarto-alternate-formats ul{padding-left:0;list-style:none}.sidebar nav[role=doc-toc] ul{list-style:none;padding-left:0;list-style:none}.sidebar nav[role=doc-toc]>ul{margin-left:.45em}.quarto-margin-sidebar nav[role=doc-toc]{padding-left:.5em}.sidebar .toc-actions>ul,.sidebar .quarto-code-links>ul,.sidebar .quarto-other-links>ul,.sidebar .quarto-alternate-notebooks>ul,.sidebar .quarto-alternate-formats>ul{font-size:.8rem}.sidebar nav[role=doc-toc]>ul{font-size:.875rem}.sidebar .toc-actions ul li a,.sidebar .quarto-code-links ul li a,.sidebar .quarto-other-links ul li a,.sidebar .quarto-alternate-notebooks ul li a,.sidebar .quarto-alternate-formats ul li a,.sidebar nav[role=doc-toc]>ul li a{line-height:1.1rem;padding-bottom:.2rem;padding-top:.2rem;color:inherit}.sidebar nav[role=doc-toc] ul>li>ul>li>a{padding-left:1.2em}.sidebar nav[role=doc-toc] ul>li>ul>li>ul>li>a{padding-left:2.4em}.sidebar nav[role=doc-toc] ul>li>ul>li>ul>li>ul>li>a{padding-left:3.6em}.sidebar nav[role=doc-toc] ul>li>ul>li>ul>li>ul>li>ul>li>a{padding-left:4.8em}.sidebar nav[role=doc-toc] ul>li>ul>li>ul>li>ul>li>ul>li>ul>li>a{padding-left:6em}.sidebar nav[role=doc-toc] ul>li>a.active,.sidebar nav[role=doc-toc] ul>li>ul>li>a.active{border-left:1px solid #4040bf;color:#4040bf !important}.sidebar nav[role=doc-toc] ul>li>a:hover,.sidebar nav[role=doc-toc] ul>li>ul>li>a:hover{color:#4040bf !important}kbd,.kbd{color:#000;background-color:#f8f9fa;border:1px solid;border-radius:5px;border-color:#dee2e6}.quarto-appendix-contents div.hanging-indent{margin-left:0em}.quarto-appendix-contents div.hanging-indent div.csl-entry{margin-left:1em;text-indent:-1em}.citation a,.footnote-ref{text-decoration:none}.footnotes ol{padding-left:1em}.tippy-content>*{margin-bottom:.7em}.tippy-content>*:last-child{margin-bottom:0}.callout{margin-top:1.25rem;margin-bottom:1.25rem;border-radius:.25rem;overflow-wrap:break-word}.callout .callout-title-container{overflow-wrap:anywhere}.callout.callout-style-simple{padding:.4em .7em;border-left:5px solid;border-right:1px solid #dee2e6;border-top:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.callout.callout-style-default{border-left:5px solid;border-right:1px solid #dee2e6;border-top:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.callout .callout-body-container{flex-grow:1}.callout.callout-style-simple .callout-body{font-size:.9rem;font-weight:400}.callout.callout-style-default .callout-body{font-size:.9rem;font-weight:400}.callout:not(.no-icon).callout-titled.callout-style-simple .callout-body{padding-left:1.6em}.callout.callout-titled>.callout-header{padding-top:.2em;margin-bottom:-0.2em}.callout.callout-style-simple>div.callout-header{border-bottom:none;font-size:.9rem;font-weight:600;opacity:75%}.callout.callout-style-default>div.callout-header{border-bottom:none;font-weight:600;opacity:85%;font-size:.9rem;padding-left:.5em;padding-right:.5em}.callout.callout-style-default .callout-body{padding-left:.5em;padding-right:.5em}.callout.callout-style-default .callout-body>:first-child{padding-top:.5rem;margin-top:0}.callout>div.callout-header[data-bs-toggle=collapse]{cursor:pointer}.callout.callout-style-default .callout-header[aria-expanded=false],.callout.callout-style-default .callout-header[aria-expanded=true]{padding-top:0px;margin-bottom:0px;align-items:center}.callout.callout-titled .callout-body>:last-child:not(.sourceCode),.callout.callout-titled .callout-body>div>:last-child:not(.sourceCode){padding-bottom:.5rem;margin-bottom:0}.callout:not(.callout-titled) .callout-body>:first-child,.callout:not(.callout-titled) .callout-body>div>:first-child{margin-top:.25rem}.callout:not(.callout-titled) .callout-body>:last-child,.callout:not(.callout-titled) .callout-body>div>:last-child{margin-bottom:.2rem}.callout.callout-style-simple .callout-icon::before,.callout.callout-style-simple .callout-toggle::before{height:1rem;width:1rem;display:inline-block;content:"";background-repeat:no-repeat;background-size:1rem 1rem}.callout.callout-style-default .callout-icon::before,.callout.callout-style-default .callout-toggle::before{height:.9rem;width:.9rem;display:inline-block;content:"";background-repeat:no-repeat;background-size:.9rem .9rem}.callout.callout-style-default .callout-toggle::before{margin-top:5px}.callout .callout-btn-toggle .callout-toggle::before{transition:transform .2s linear}.callout .callout-header[aria-expanded=false] .callout-toggle::before{transform:rotate(-90deg)}.callout .callout-header[aria-expanded=true] .callout-toggle::before{transform:none}.callout.callout-style-simple:not(.no-icon) div.callout-icon-container{padding-top:.2em;padding-right:.55em}.callout.callout-style-default:not(.no-icon) div.callout-icon-container{padding-top:.1em;padding-right:.35em}.callout.callout-style-default:not(.no-icon) div.callout-title-container{margin-top:-1px}.callout.callout-style-default.callout-caution:not(.no-icon) div.callout-icon-container{padding-top:.3em;padding-right:.35em}.callout>.callout-body>.callout-icon-container>.no-icon,.callout>.callout-header>.callout-icon-container>.no-icon{display:none}div.callout.callout{border-left-color:#6c757d}div.callout.callout-style-default>.callout-header{background-color:#6c757d}div.callout-note.callout{border-left-color:#2780e3}div.callout-note.callout-style-default>.callout-header{background-color:#e9f2fc}div.callout-note:not(.callout-titled) .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-note.callout-titled .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-note .callout-toggle::before{background-image:url('data:image/svg+xml,')}div.callout-tip.callout{border-left-color:#3fb618}div.callout-tip.callout-style-default>.callout-header{background-color:#ecf8e8}div.callout-tip:not(.callout-titled) .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-tip.callout-titled .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-tip .callout-toggle::before{background-image:url('data:image/svg+xml,')}div.callout-warning.callout{border-left-color:#ff7518}div.callout-warning.callout-style-default>.callout-header{background-color:#fff1e8}div.callout-warning:not(.callout-titled) .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-warning.callout-titled .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-warning .callout-toggle::before{background-image:url('data:image/svg+xml,')}div.callout-caution.callout{border-left-color:#f0ad4e}div.callout-caution.callout-style-default>.callout-header{background-color:#fef7ed}div.callout-caution:not(.callout-titled) .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-caution.callout-titled .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-caution .callout-toggle::before{background-image:url('data:image/svg+xml,')}div.callout-important.callout{border-left-color:#ff0039}div.callout-important.callout-style-default>.callout-header{background-color:#ffe6eb}div.callout-important:not(.callout-titled) .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-important.callout-titled .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-important .callout-toggle::before{background-image:url('data:image/svg+xml,')}.quarto-toggle-container{display:flex;align-items:center}.quarto-reader-toggle .bi::before,.quarto-color-scheme-toggle .bi::before{display:inline-block;height:1rem;width:1rem;content:"";background-repeat:no-repeat;background-size:1rem 1rem}.sidebar-navigation{padding-left:20px}.navbar{background-color:#3cdd8c;color:#fff}.navbar .quarto-color-scheme-toggle:not(.alternate) .bi::before{background-image:url('data:image/svg+xml,')}.navbar .quarto-color-scheme-toggle.alternate .bi::before{background-image:url('data:image/svg+xml,')}.sidebar-navigation .quarto-color-scheme-toggle:not(.alternate) .bi::before{background-image:url('data:image/svg+xml,')}.sidebar-navigation .quarto-color-scheme-toggle.alternate .bi::before{background-image:url('data:image/svg+xml,')}.quarto-sidebar-toggle{border-color:#dee2e6;border-bottom-left-radius:.25rem;border-bottom-right-radius:.25rem;border-style:solid;border-width:1px;overflow:hidden;border-top-width:0px;padding-top:0px !important}.quarto-sidebar-toggle-title{cursor:pointer;padding-bottom:2px;margin-left:.25em;text-align:center;font-weight:400;font-size:.775em}#quarto-content .quarto-sidebar-toggle{background:#fafafa}#quarto-content .quarto-sidebar-toggle-title{color:#000}.quarto-sidebar-toggle-icon{color:#dee2e6;margin-right:.5em;float:right;transition:transform .2s ease}.quarto-sidebar-toggle-icon::before{padding-top:5px}.quarto-sidebar-toggle.expanded .quarto-sidebar-toggle-icon{transform:rotate(-180deg)}.quarto-sidebar-toggle.expanded .quarto-sidebar-toggle-title{border-bottom:solid #dee2e6 1px}.quarto-sidebar-toggle-contents{background-color:#fff;padding-right:10px;padding-left:10px;margin-top:0px !important;transition:max-height .5s ease}.quarto-sidebar-toggle.expanded .quarto-sidebar-toggle-contents{padding-top:1em;padding-bottom:10px}@media(max-width: 767.98px){.sidebar-menu-container{padding-bottom:5em}}.quarto-sidebar-toggle:not(.expanded) .quarto-sidebar-toggle-contents{padding-top:0px !important;padding-bottom:0px}nav[role=doc-toc]{z-index:1020}#quarto-sidebar>*,nav[role=doc-toc]>*{transition:opacity .1s ease,border .1s ease}#quarto-sidebar.slow>*,nav[role=doc-toc].slow>*{transition:opacity .4s ease,border .4s ease}.quarto-color-scheme-toggle:not(.alternate).top-right .bi::before{background-image:url('data:image/svg+xml,')}.quarto-color-scheme-toggle.alternate.top-right .bi::before{background-image:url('data:image/svg+xml,')}#quarto-appendix.default{border-top:1px solid #dee2e6}#quarto-appendix.default{background-color:#fff;padding-top:1.5em;margin-top:2em;z-index:998}#quarto-appendix.default .quarto-appendix-heading{margin-top:0;line-height:1.4em;font-weight:600;opacity:.9;border-bottom:none;margin-bottom:0}#quarto-appendix.default .footnotes ol,#quarto-appendix.default .footnotes ol li>p:last-of-type,#quarto-appendix.default .quarto-appendix-contents>p:last-of-type{margin-bottom:0}#quarto-appendix.default .footnotes ol{margin-left:.5em}#quarto-appendix.default .quarto-appendix-secondary-label{margin-bottom:.4em}#quarto-appendix.default .quarto-appendix-bibtex{font-size:.7em;padding:1em;border:solid 1px #dee2e6;margin-bottom:1em}#quarto-appendix.default .quarto-appendix-bibtex code.sourceCode{white-space:pre-wrap}#quarto-appendix.default .quarto-appendix-citeas{font-size:.9em;padding:1em;border:solid 1px #dee2e6;margin-bottom:1em}#quarto-appendix.default .quarto-appendix-heading{font-size:1em !important}#quarto-appendix.default *[role=doc-endnotes]>ol,#quarto-appendix.default .quarto-appendix-contents>*:not(h2):not(.h2){font-size:.9em}#quarto-appendix.default section{padding-bottom:1.5em}#quarto-appendix.default section *[role=doc-endnotes],#quarto-appendix.default section>*:not(a){opacity:.9;word-wrap:break-word}.btn.btn-quarto,div.cell-output-display .btn-quarto{--bs-btn-color: #cacccd;--bs-btn-bg: #343a40;--bs-btn-border-color: #343a40;--bs-btn-hover-color: #cacccd;--bs-btn-hover-bg: #52585d;--bs-btn-hover-border-color: #484e53;--bs-btn-focus-shadow-rgb: 75, 80, 85;--bs-btn-active-color: #fff;--bs-btn-active-bg: #5d6166;--bs-btn-active-border-color: #484e53;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #343a40;--bs-btn-disabled-border-color: #343a40}nav.quarto-secondary-nav.color-navbar{background-color:#3cdd8c;color:#fff}nav.quarto-secondary-nav.color-navbar h1,nav.quarto-secondary-nav.color-navbar .h1,nav.quarto-secondary-nav.color-navbar .quarto-btn-toggle{color:#fff}@media(max-width: 991.98px){body.nav-sidebar .quarto-title-banner{margin-bottom:0;padding-bottom:1em}body.nav-sidebar #title-block-header{margin-block-end:0}}p.subtitle{margin-top:.25em;margin-bottom:.5em}code a:any-link{color:inherit;text-decoration-color:#6c757d}/*! light */div.observablehq table thead tr th{background-color:var(--bs-body-bg)}input,button,select,optgroup,textarea{background-color:var(--bs-body-bg)}.code-annotated .code-copy-button{margin-right:1.25em;margin-top:0;padding-bottom:0;padding-top:3px}.code-annotation-gutter-bg{background-color:#fff}.code-annotation-gutter{background-color:rgba(233,236,239,.65)}.code-annotation-gutter,.code-annotation-gutter-bg{height:100%;width:calc(20px + .5em);position:absolute;top:0;right:0}dl.code-annotation-container-grid dt{margin-right:1em;margin-top:.25rem}dl.code-annotation-container-grid dt{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;color:#1a1a1a;border:solid #1a1a1a 1px;border-radius:50%;height:22px;width:22px;line-height:22px;font-size:11px;text-align:center;vertical-align:middle;text-decoration:none}dl.code-annotation-container-grid dt[data-target-cell]{cursor:pointer}dl.code-annotation-container-grid dt[data-target-cell].code-annotation-active{color:#fff;border:solid #aaa 1px;background-color:#aaa}pre.code-annotation-code{padding-top:0;padding-bottom:0}pre.code-annotation-code code{z-index:3}#code-annotation-line-highlight-gutter{width:100%;border-top:solid rgba(170,170,170,.2666666667) 1px;border-bottom:solid rgba(170,170,170,.2666666667) 1px;z-index:2;background-color:rgba(170,170,170,.1333333333)}#code-annotation-line-highlight{margin-left:-4em;width:calc(100% + 4em);border-top:solid rgba(170,170,170,.2666666667) 1px;border-bottom:solid rgba(170,170,170,.2666666667) 1px;z-index:2;background-color:rgba(170,170,170,.1333333333)}code.sourceCode .code-annotation-anchor.code-annotation-active{background-color:var(--quarto-hl-normal-color, #aaaaaa);border:solid var(--quarto-hl-normal-color, #aaaaaa) 1px;color:#e9ecef;font-weight:bolder}code.sourceCode .code-annotation-anchor{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;color:var(--quarto-hl-co-color);border:solid var(--quarto-hl-co-color) 1px;border-radius:50%;height:18px;width:18px;font-size:9px;margin-top:2px}code.sourceCode button.code-annotation-anchor{padding:2px;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none}code.sourceCode a.code-annotation-anchor{line-height:18px;text-align:center;vertical-align:middle;cursor:default;text-decoration:none}@media print{.page-columns .column-screen-inset{grid-column:page-start-inset/page-end-inset;z-index:998;opacity:.999}.page-columns .column-screen-inset table{background:#fff}.page-columns .column-screen-inset-left{grid-column:page-start-inset/body-content-end;z-index:998;opacity:.999}.page-columns .column-screen-inset-left table{background:#fff}.page-columns .column-screen-inset-right{grid-column:body-content-start/page-end-inset;z-index:998;opacity:.999}.page-columns .column-screen-inset-right table{background:#fff}.page-columns .column-screen{grid-column:page-start/page-end;z-index:998;opacity:.999}.page-columns .column-screen table{background:#fff}.page-columns .column-screen-left{grid-column:page-start/body-content-end;z-index:998;opacity:.999}.page-columns .column-screen-left table{background:#fff}.page-columns .column-screen-right{grid-column:body-content-start/page-end;z-index:998;opacity:.999}.page-columns .column-screen-right table{background:#fff}.page-columns .column-screen-inset-shaded{grid-column:page-start-inset/page-end-inset;padding:1em;background:#f8f9fa;z-index:998;opacity:.999;margin-bottom:1em}}.quarto-video{margin-bottom:1em}.table{border-top:1px solid #b3b3b3;border-bottom:1px solid #b3b3b3}.table>thead{border-top-width:0;border-bottom:1px solid gray}.table a{word-break:break-word}.table>:not(caption)>*>*{background-color:unset;color:unset}#quarto-document-content .crosstalk-input .checkbox input[type=checkbox],#quarto-document-content .crosstalk-input .checkbox-inline input[type=checkbox]{position:unset;margin-top:unset;margin-left:unset}#quarto-document-content .row{margin-left:unset;margin-right:unset}.quarto-xref{white-space:nowrap}#quarto-draft-alert{margin-top:0px;margin-bottom:0px;padding:.3em;text-align:center;font-size:.9em}#quarto-draft-alert i{margin-right:.3em}a.external:after{content:"";background-image:url('data:image/svg+xml,');background-size:contain;background-repeat:no-repeat;background-position:center center;margin-left:.2em;padding-right:.75em}div.sourceCode code a.external:after{content:none}a.external:after:hover{cursor:pointer}.quarto-ext-icon{display:inline-block;font-size:.75em;padding-left:.3em}.code-with-filename .code-with-filename-file{margin-bottom:0;padding-bottom:2px;padding-top:2px;padding-left:.7em;border:var(--quarto-border-width) solid var(--quarto-border-color);border-radius:var(--quarto-border-radius);border-bottom:0;border-bottom-left-radius:0%;border-bottom-right-radius:0%}.code-with-filename div.sourceCode,.reveal .code-with-filename div.sourceCode{margin-top:0;border-top-left-radius:0%;border-top-right-radius:0%}.code-with-filename .code-with-filename-file pre{margin-bottom:0}.code-with-filename .code-with-filename-file{background-color:rgba(219,219,219,.8)}.quarto-dark .code-with-filename .code-with-filename-file{background-color:#555}.code-with-filename .code-with-filename-file strong{font-weight:400}.quarto-title-banner{margin-bottom:1em;color:#fff;background:#3cdd8c}.quarto-title-banner a{color:#fff}.quarto-title-banner h1,.quarto-title-banner .h1,.quarto-title-banner h2,.quarto-title-banner .h2{color:#fff}.quarto-title-banner .code-tools-button{color:#fff}.quarto-title-banner .code-tools-button:hover{color:#fff}.quarto-title-banner .code-tools-button>.bi::before{background-image:url('data:image/svg+xml,')}.quarto-title-banner .code-tools-button:hover>.bi::before{background-image:url('data:image/svg+xml,')}.quarto-title-banner .quarto-title .title{font-weight:600}.quarto-title-banner .quarto-categories{margin-top:.75em}@media(min-width: 992px){.quarto-title-banner{padding-top:2.5em;padding-bottom:2.5em}}@media(max-width: 991.98px){.quarto-title-banner{padding-top:1em;padding-bottom:1em}}@media(max-width: 767.98px){body.hypothesis-enabled #title-block-header>*{padding-right:20px}}main.quarto-banner-title-block>section:first-child>h2,main.quarto-banner-title-block>section:first-child>.h2,main.quarto-banner-title-block>section:first-child>h3,main.quarto-banner-title-block>section:first-child>.h3,main.quarto-banner-title-block>section:first-child>h4,main.quarto-banner-title-block>section:first-child>.h4{margin-top:0}.quarto-title .quarto-categories{display:flex;flex-wrap:wrap;row-gap:.5em;column-gap:.4em;padding-bottom:.5em;margin-top:.75em}.quarto-title .quarto-categories .quarto-category{padding:.25em .75em;font-size:.65em;text-transform:uppercase;border:solid 1px;border-radius:.25rem;opacity:.6}.quarto-title .quarto-categories .quarto-category a{color:inherit}.quarto-title-meta-container{display:grid;grid-template-columns:1fr auto}.quarto-title-meta-column-end{display:flex;flex-direction:column;padding-left:1em}.quarto-title-meta-column-end a .bi{margin-right:.3em}#title-block-header.quarto-title-block.default .quarto-title-meta{display:grid;grid-template-columns:repeat(2, 1fr);grid-column-gap:1em}#title-block-header.quarto-title-block.default .quarto-title .title{margin-bottom:0}#title-block-header.quarto-title-block.default .quarto-title-author-orcid img{margin-top:-0.2em;height:.8em;width:.8em}#title-block-header.quarto-title-block.default .quarto-title-author-email{opacity:.7}#title-block-header.quarto-title-block.default .quarto-description p:last-of-type{margin-bottom:0}#title-block-header.quarto-title-block.default .quarto-title-meta-contents p,#title-block-header.quarto-title-block.default .quarto-title-authors p,#title-block-header.quarto-title-block.default .quarto-title-affiliations p{margin-bottom:.1em}#title-block-header.quarto-title-block.default .quarto-title-meta-heading{text-transform:uppercase;margin-top:1em;font-size:.8em;opacity:.8;font-weight:400}#title-block-header.quarto-title-block.default .quarto-title-meta-contents{font-size:.9em}#title-block-header.quarto-title-block.default .quarto-title-meta-contents p.affiliation:last-of-type{margin-bottom:.1em}#title-block-header.quarto-title-block.default p.affiliation{margin-bottom:.1em}#title-block-header.quarto-title-block.default .keywords,#title-block-header.quarto-title-block.default .description,#title-block-header.quarto-title-block.default .abstract{margin-top:0}#title-block-header.quarto-title-block.default .keywords>p,#title-block-header.quarto-title-block.default .description>p,#title-block-header.quarto-title-block.default .abstract>p{font-size:.9em}#title-block-header.quarto-title-block.default .keywords>p:last-of-type,#title-block-header.quarto-title-block.default .description>p:last-of-type,#title-block-header.quarto-title-block.default .abstract>p:last-of-type{margin-bottom:0}#title-block-header.quarto-title-block.default .keywords .block-title,#title-block-header.quarto-title-block.default .description .block-title,#title-block-header.quarto-title-block.default .abstract .block-title{margin-top:1em;text-transform:uppercase;font-size:.8em;opacity:.8;font-weight:400}#title-block-header.quarto-title-block.default .quarto-title-meta-author{display:grid;grid-template-columns:minmax(max-content, 1fr) 1fr;grid-column-gap:1em}.quarto-title-tools-only{display:flex;justify-content:right}body{-webkit-font-smoothing:antialiased}.badge.bg-light{color:#343a40}.progress .progress-bar{font-size:8px;line-height:8px}
diff --git a/site_libs/bootstrap/bootstrap.min.js b/site_libs/bootstrap/bootstrap.min.js
new file mode 100644
index 00000000..e8f21f70
--- /dev/null
+++ b/site_libs/bootstrap/bootstrap.min.js
@@ -0,0 +1,7 @@
+/*!
+ * Bootstrap v5.3.1 (https://getbootstrap.com/)
+ * Copyright 2011-2023 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ */
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t=new Map,e={set(e,i,n){t.has(e)||t.set(e,new Map);const s=t.get(e);s.has(i)||0===s.size?s.set(i,n):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(e,i)=>t.has(e)&&t.get(e).get(i)||null,remove(e,i){if(!t.has(e))return;const n=t.get(e);n.delete(i),0===n.size&&t.delete(e)}},i="transitionend",n=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),s=t=>{t.dispatchEvent(new Event(i))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(n(t)):null,a=t=>{if(!o(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},l=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),c=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?c(t.parentNode):null},h=()=>{},d=t=>{t.offsetHeight},u=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,f=[],p=()=>"rtl"===document.documentElement.dir,m=t=>{var e;e=()=>{const e=u();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(f.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of f)t()})),f.push(e)):e()},g=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,_=(t,e,n=!0)=>{if(!n)return void g(t);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let r=!1;const a=({target:n})=>{n===e&&(r=!0,e.removeEventListener(i,a),g(t))};e.addEventListener(i,a),setTimeout((()=>{r||s(e)}),o)},b=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,A={};let E=1;const T={mouseenter:"mouseover",mouseleave:"mouseout"},C=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${E++}`||t.uidEvent||E++}function x(t){const e=O(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function k(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function L(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=I(t);return C.has(o)||(o=t),[n,s,o]}function S(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=L(e,i,n);if(e in T){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=x(t),c=l[a]||(l[a]={}),h=k(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=O(r,e.replace(v,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return P(s,{delegateTarget:r}),n.oneOff&&N.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return P(n,{delegateTarget:t}),i.oneOff&&N.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function D(t,e,i,n,s){const o=k(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function $(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&D(t,e,i,r.callable,r.delegationSelector)}function I(t){return t=t.replace(y,""),T[t]||t}const N={on(t,e,i,n){S(t,e,i,n,!1)},one(t,e,i,n){S(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=L(e,i,n),a=r!==e,l=x(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))$(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(w,"");a&&!e.includes(s)||D(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;D(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=u();let s=null,o=!0,r=!0,a=!1;e!==I(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=P(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function P(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function M(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function j(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const F={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${j(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${j(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=M(t.dataset[n])}return e},getDataAttribute:(t,e)=>M(t.getAttribute(`data-bs-${j(e)}`))};class H{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=o(e)?F.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...o(e)?F.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[n,s]of Object.entries(e)){const e=t[n],r=o(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(r))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${r}" but expected type "${s}".`)}var i}}class W extends H{constructor(t,i){super(),(t=r(t))&&(this._element=t,this._config=this._getConfig(i),e.set(this._element,this.constructor.DATA_KEY,this))}dispose(){e.remove(this._element,this.constructor.DATA_KEY),N.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){_(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return e.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.1"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const B=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return n(e)},z={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!l(t)&&a(t)))},getSelectorFromElement(t){const e=B(t);return e&&z.findOne(e)?e:null},getElementFromSelector(t){const e=B(t);return e?z.findOne(e):null},getMultipleElementsFromSelector(t){const e=B(t);return e?z.find(e):[]}},R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;N.on(document,i,`[data-bs-dismiss="${n}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),l(this))return;const s=z.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()}))},q=".bs.alert",V=`close${q}`,K=`closed${q}`;class Q extends W{static get NAME(){return"alert"}close(){if(N.trigger(this._element,V).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),N.trigger(this._element,K),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=Q.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}R(Q,"close"),m(Q);const X='[data-bs-toggle="button"]';class Y extends W{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=Y.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}N.on(document,"click.bs.button.data-api",X,(t=>{t.preventDefault();const e=t.target.closest(X);Y.getOrCreateInstance(e).toggle()})),m(Y);const U=".bs.swipe",G=`touchstart${U}`,J=`touchmove${U}`,Z=`touchend${U}`,tt=`pointerdown${U}`,et=`pointerup${U}`,it={endCallback:null,leftCallback:null,rightCallback:null},nt={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class st extends H{constructor(t,e){super(),this._element=t,t&&st.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return it}static get DefaultType(){return nt}static get NAME(){return"swipe"}dispose(){N.off(this._element,U)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),g(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&g(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(N.on(this._element,tt,(t=>this._start(t))),N.on(this._element,et,(t=>this._end(t))),this._element.classList.add("pointer-event")):(N.on(this._element,G,(t=>this._start(t))),N.on(this._element,J,(t=>this._move(t))),N.on(this._element,Z,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const ot=".bs.carousel",rt=".data-api",at="next",lt="prev",ct="left",ht="right",dt=`slide${ot}`,ut=`slid${ot}`,ft=`keydown${ot}`,pt=`mouseenter${ot}`,mt=`mouseleave${ot}`,gt=`dragstart${ot}`,_t=`load${ot}${rt}`,bt=`click${ot}${rt}`,vt="carousel",yt="active",wt=".active",At=".carousel-item",Et=wt+At,Tt={ArrowLeft:ht,ArrowRight:ct},Ct={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},Ot={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class xt extends W{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=z.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===vt&&this.cycle()}static get Default(){return Ct}static get DefaultType(){return Ot}static get NAME(){return"carousel"}next(){this._slide(at)}nextWhenVisible(){!document.hidden&&a(this._element)&&this.next()}prev(){this._slide(lt)}pause(){this._isSliding&&s(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?N.one(this._element,ut,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void N.one(this._element,ut,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?at:lt;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&N.on(this._element,ft,(t=>this._keydown(t))),"hover"===this._config.pause&&(N.on(this._element,pt,(()=>this.pause())),N.on(this._element,mt,(()=>this._maybeEnableCycle()))),this._config.touch&&st.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of z.find(".carousel-item img",this._element))N.on(t,gt,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(ct)),rightCallback:()=>this._slide(this._directionToOrder(ht)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new st(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=Tt[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=z.findOne(wt,this._indicatorsElement);e.classList.remove(yt),e.removeAttribute("aria-current");const i=z.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(yt),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===at,s=e||b(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>N.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(dt).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),d(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(yt),i.classList.remove(yt,c,l),this._isSliding=!1,r(ut)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return z.findOne(Et,this._element)}_getItems(){return z.find(At,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===ct?lt:at:t===ct?at:lt}_orderToDirection(t){return p()?t===lt?ct:ht:t===lt?ht:ct}static jQueryInterface(t){return this.each((function(){const e=xt.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}N.on(document,bt,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=z.getElementFromSelector(this);if(!e||!e.classList.contains(vt))return;t.preventDefault();const i=xt.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===F.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),N.on(window,_t,(()=>{const t=z.find('[data-bs-ride="carousel"]');for(const e of t)xt.getOrCreateInstance(e)})),m(xt);const kt=".bs.collapse",Lt=`show${kt}`,St=`shown${kt}`,Dt=`hide${kt}`,$t=`hidden${kt}`,It=`click${kt}.data-api`,Nt="show",Pt="collapse",Mt="collapsing",jt=`:scope .${Pt} .${Pt}`,Ft='[data-bs-toggle="collapse"]',Ht={parent:null,toggle:!0},Wt={parent:"(null|element)",toggle:"boolean"};class Bt extends W{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=z.find(Ft);for(const t of i){const e=z.getSelectorFromElement(t),i=z.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Ht}static get DefaultType(){return Wt}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>Bt.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(N.trigger(this._element,Lt).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(Pt),this._element.classList.add(Mt),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(Mt),this._element.classList.add(Pt,Nt),this._element.style[e]="",N.trigger(this._element,St)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(N.trigger(this._element,Dt).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,d(this._element),this._element.classList.add(Mt),this._element.classList.remove(Pt,Nt);for(const t of this._triggerArray){const e=z.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(Mt),this._element.classList.add(Pt),N.trigger(this._element,$t)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(Nt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=r(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Ft);for(const e of t){const t=z.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=z.find(jt,this._config.parent);return z.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=Bt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}N.on(document,It,Ft,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of z.getMultipleElementsFromSelector(this))Bt.getOrCreateInstance(t,{toggle:!1}).toggle()})),m(Bt);var zt="top",Rt="bottom",qt="right",Vt="left",Kt="auto",Qt=[zt,Rt,qt,Vt],Xt="start",Yt="end",Ut="clippingParents",Gt="viewport",Jt="popper",Zt="reference",te=Qt.reduce((function(t,e){return t.concat([e+"-"+Xt,e+"-"+Yt])}),[]),ee=[].concat(Qt,[Kt]).reduce((function(t,e){return t.concat([e,e+"-"+Xt,e+"-"+Yt])}),[]),ie="beforeRead",ne="read",se="afterRead",oe="beforeMain",re="main",ae="afterMain",le="beforeWrite",ce="write",he="afterWrite",de=[ie,ne,se,oe,re,ae,le,ce,he];function ue(t){return t?(t.nodeName||"").toLowerCase():null}function fe(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function pe(t){return t instanceof fe(t).Element||t instanceof Element}function me(t){return t instanceof fe(t).HTMLElement||t instanceof HTMLElement}function ge(t){return"undefined"!=typeof ShadowRoot&&(t instanceof fe(t).ShadowRoot||t instanceof ShadowRoot)}const _e={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];me(s)&&ue(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});me(n)&&ue(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function be(t){return t.split("-")[0]}var ve=Math.max,ye=Math.min,we=Math.round;function Ae(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function Ee(){return!/^((?!chrome|android).)*safari/i.test(Ae())}function Te(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&me(t)&&(s=t.offsetWidth>0&&we(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&we(n.height)/t.offsetHeight||1);var r=(pe(t)?fe(t):window).visualViewport,a=!Ee()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function Ce(t){var e=Te(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Oe(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&ge(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function xe(t){return fe(t).getComputedStyle(t)}function ke(t){return["table","td","th"].indexOf(ue(t))>=0}function Le(t){return((pe(t)?t.ownerDocument:t.document)||window.document).documentElement}function Se(t){return"html"===ue(t)?t:t.assignedSlot||t.parentNode||(ge(t)?t.host:null)||Le(t)}function De(t){return me(t)&&"fixed"!==xe(t).position?t.offsetParent:null}function $e(t){for(var e=fe(t),i=De(t);i&&ke(i)&&"static"===xe(i).position;)i=De(i);return i&&("html"===ue(i)||"body"===ue(i)&&"static"===xe(i).position)?e:i||function(t){var e=/firefox/i.test(Ae());if(/Trident/i.test(Ae())&&me(t)&&"fixed"===xe(t).position)return null;var i=Se(t);for(ge(i)&&(i=i.host);me(i)&&["html","body"].indexOf(ue(i))<0;){var n=xe(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Ie(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function Ne(t,e,i){return ve(t,ye(e,i))}function Pe(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function Me(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const je={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=be(i.placement),l=Ie(a),c=[Vt,qt].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return Pe("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:Me(t,Qt))}(s.padding,i),d=Ce(o),u="y"===l?zt:Vt,f="y"===l?Rt:qt,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=$e(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,A=Ne(v,w,y),E=l;i.modifiersData[n]=((e={})[E]=A,e.centerOffset=A-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Oe(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Fe(t){return t.split("-")[1]}var He={top:"auto",right:"auto",bottom:"auto",left:"auto"};function We(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=t.isFixed,u=r.x,f=void 0===u?0:u,p=r.y,m=void 0===p?0:p,g="function"==typeof h?h({x:f,y:m}):{x:f,y:m};f=g.x,m=g.y;var _=r.hasOwnProperty("x"),b=r.hasOwnProperty("y"),v=Vt,y=zt,w=window;if(c){var A=$e(i),E="clientHeight",T="clientWidth";A===fe(i)&&"static"!==xe(A=Le(i)).position&&"absolute"===a&&(E="scrollHeight",T="scrollWidth"),(s===zt||(s===Vt||s===qt)&&o===Yt)&&(y=Rt,m-=(d&&A===w&&w.visualViewport?w.visualViewport.height:A[E])-n.height,m*=l?1:-1),s!==Vt&&(s!==zt&&s!==Rt||o!==Yt)||(v=qt,f-=(d&&A===w&&w.visualViewport?w.visualViewport.width:A[T])-n.width,f*=l?1:-1)}var C,O=Object.assign({position:a},c&&He),x=!0===h?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:we(i*s)/s||0,y:we(n*s)/s||0}}({x:f,y:m},fe(i)):{x:f,y:m};return f=x.x,m=x.y,l?Object.assign({},O,((C={})[y]=b?"0":"",C[v]=_?"0":"",C.transform=(w.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",C)):Object.assign({},O,((e={})[y]=b?m+"px":"",e[v]=_?f+"px":"",e.transform="",e))}const Be={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:be(e.placement),variation:Fe(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,We(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,We(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var ze={passive:!0};const Re={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=fe(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,ze)})),a&&l.addEventListener("resize",i.update,ze),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,ze)})),a&&l.removeEventListener("resize",i.update,ze)}},data:{}};var qe={left:"right",right:"left",bottom:"top",top:"bottom"};function Ve(t){return t.replace(/left|right|bottom|top/g,(function(t){return qe[t]}))}var Ke={start:"end",end:"start"};function Qe(t){return t.replace(/start|end/g,(function(t){return Ke[t]}))}function Xe(t){var e=fe(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Ye(t){return Te(Le(t)).left+Xe(t).scrollLeft}function Ue(t){var e=xe(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ge(t){return["html","body","#document"].indexOf(ue(t))>=0?t.ownerDocument.body:me(t)&&Ue(t)?t:Ge(Se(t))}function Je(t,e){var i;void 0===e&&(e=[]);var n=Ge(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=fe(n),r=s?[o].concat(o.visualViewport||[],Ue(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Je(Se(r)))}function Ze(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function ti(t,e,i){return e===Gt?Ze(function(t,e){var i=fe(t),n=Le(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=Ee();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+Ye(t),y:l}}(t,i)):pe(e)?function(t,e){var i=Te(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):Ze(function(t){var e,i=Le(t),n=Xe(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=ve(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=ve(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+Ye(t),l=-n.scrollTop;return"rtl"===xe(s||i).direction&&(a+=ve(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Le(t)))}function ei(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?be(s):null,r=s?Fe(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case zt:e={x:a,y:i.y-n.height};break;case Rt:e={x:a,y:i.y+i.height};break;case qt:e={x:i.x+i.width,y:l};break;case Vt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?Ie(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case Xt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Yt:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function ii(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.strategy,r=void 0===o?t.strategy:o,a=i.boundary,l=void 0===a?Ut:a,c=i.rootBoundary,h=void 0===c?Gt:c,d=i.elementContext,u=void 0===d?Jt:d,f=i.altBoundary,p=void 0!==f&&f,m=i.padding,g=void 0===m?0:m,_=Pe("number"!=typeof g?g:Me(g,Qt)),b=u===Jt?Zt:Jt,v=t.rects.popper,y=t.elements[p?b:u],w=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=Je(Se(t)),i=["absolute","fixed"].indexOf(xe(t).position)>=0&&me(t)?$e(t):t;return pe(i)?e.filter((function(t){return pe(t)&&Oe(t,i)&&"body"!==ue(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=ti(t,i,n);return e.top=ve(s.top,e.top),e.right=ye(s.right,e.right),e.bottom=ye(s.bottom,e.bottom),e.left=ve(s.left,e.left),e}),ti(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(pe(y)?y:y.contextElement||Le(t.elements.popper),l,h,r),A=Te(t.elements.reference),E=ei({reference:A,element:v,strategy:"absolute",placement:s}),T=Ze(Object.assign({},v,E)),C=u===Jt?T:A,O={top:w.top-C.top+_.top,bottom:C.bottom-w.bottom+_.bottom,left:w.left-C.left+_.left,right:C.right-w.right+_.right},x=t.modifiersData.offset;if(u===Jt&&x){var k=x[s];Object.keys(O).forEach((function(t){var e=[qt,Rt].indexOf(t)>=0?1:-1,i=[zt,Rt].indexOf(t)>=0?"y":"x";O[t]+=k[i]*e}))}return O}function ni(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?ee:l,h=Fe(n),d=h?a?te:te.filter((function(t){return Fe(t)===h})):Qt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=ii(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[be(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const si={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=be(g),b=l||(_!==g&&p?function(t){if(be(t)===Kt)return[];var e=Ve(t);return[Qe(t),e,Qe(e)]}(g):[Ve(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(be(i)===Kt?ni(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,A=new Map,E=!0,T=v[0],C=0;C=0,S=L?"width":"height",D=ii(e,{placement:O,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),$=L?k?qt:Vt:k?Rt:zt;y[S]>w[S]&&($=Ve($));var I=Ve($),N=[];if(o&&N.push(D[x]<=0),a&&N.push(D[$]<=0,D[I]<=0),N.every((function(t){return t}))){T=O,E=!1;break}A.set(O,N)}if(E)for(var P=function(t){var e=v.find((function(e){var i=A.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},M=p?3:1;M>0&&"break"!==P(M);M--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function oi(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function ri(t){return[zt,qt,Rt,Vt].some((function(e){return t[e]>=0}))}const ai={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=ii(e,{elementContext:"reference"}),a=ii(e,{altBoundary:!0}),l=oi(r,n),c=oi(a,s,o),h=ri(l),d=ri(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},li={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=ee.reduce((function(t,i){return t[i]=function(t,e,i){var n=be(t),s=[Vt,zt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[Vt,qt].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},ci={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=ei({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},hi={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=ii(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=be(e.placement),b=Fe(e.placement),v=!b,y=Ie(_),w="x"===y?"y":"x",A=e.modifiersData.popperOffsets,E=e.rects.reference,T=e.rects.popper,C="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,O="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),x=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,k={x:0,y:0};if(A){if(o){var L,S="y"===y?zt:Vt,D="y"===y?Rt:qt,$="y"===y?"height":"width",I=A[y],N=I+g[S],P=I-g[D],M=f?-T[$]/2:0,j=b===Xt?E[$]:T[$],F=b===Xt?-T[$]:-E[$],H=e.elements.arrow,W=f&&H?Ce(H):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},z=B[S],R=B[D],q=Ne(0,E[$],W[$]),V=v?E[$]/2-M-q-z-O.mainAxis:j-q-z-O.mainAxis,K=v?-E[$]/2+M+q+R+O.mainAxis:F+q+R+O.mainAxis,Q=e.elements.arrow&&$e(e.elements.arrow),X=Q?"y"===y?Q.clientTop||0:Q.clientLeft||0:0,Y=null!=(L=null==x?void 0:x[y])?L:0,U=I+K-Y,G=Ne(f?ye(N,I+V-Y-X):N,I,f?ve(P,U):P);A[y]=G,k[y]=G-I}if(a){var J,Z="x"===y?zt:Vt,tt="x"===y?Rt:qt,et=A[w],it="y"===w?"height":"width",nt=et+g[Z],st=et-g[tt],ot=-1!==[zt,Vt].indexOf(_),rt=null!=(J=null==x?void 0:x[w])?J:0,at=ot?nt:et-E[it]-T[it]-rt+O.altAxis,lt=ot?et+E[it]+T[it]-rt-O.altAxis:st,ct=f&&ot?function(t,e,i){var n=Ne(t,e,i);return n>i?i:n}(at,et,lt):Ne(f?at:nt,et,f?lt:st);A[w]=ct,k[w]=ct-et}e.modifiersData[n]=k}},requiresIfExists:["offset"]};function di(t,e,i){void 0===i&&(i=!1);var n,s,o=me(e),r=me(e)&&function(t){var e=t.getBoundingClientRect(),i=we(e.width)/t.offsetWidth||1,n=we(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=Le(e),l=Te(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==ue(e)||Ue(a))&&(c=(n=e)!==fe(n)&&me(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:Xe(n)),me(e)?((h=Te(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=Ye(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function ui(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var fi={placement:"bottom",modifiers:[],strategy:"absolute"};function pi(){for(var t=arguments.length,e=new Array(t),i=0;iNumber.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(F.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...g(this._config.popperConfig,[t])}}_selectMenuItem({key:t,target:e}){const i=z.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>a(t)));i.length&&b(i,e,t===Ti,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=qi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=z.find(Ni);for(const i of e){const e=qi.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Ei,Ti].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Ii)?this:z.prev(this,Ii)[0]||z.next(this,Ii)[0]||z.findOne(Ii,t.delegateTarget.parentNode),o=qi.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}N.on(document,Si,Ii,qi.dataApiKeydownHandler),N.on(document,Si,Pi,qi.dataApiKeydownHandler),N.on(document,Li,qi.clearMenus),N.on(document,Di,qi.clearMenus),N.on(document,Li,Ii,(function(t){t.preventDefault(),qi.getOrCreateInstance(this).toggle()})),m(qi);const Vi="backdrop",Ki="show",Qi=`mousedown.bs.${Vi}`,Xi={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Yi={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Ui extends H{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Xi}static get DefaultType(){return Yi}static get NAME(){return Vi}show(t){if(!this._config.isVisible)return void g(t);this._append();const e=this._getElement();this._config.isAnimated&&d(e),e.classList.add(Ki),this._emulateAnimation((()=>{g(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(Ki),this._emulateAnimation((()=>{this.dispose(),g(t)}))):g(t)}dispose(){this._isAppended&&(N.off(this._element,Qi),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=r(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),N.on(t,Qi,(()=>{g(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){_(t,this._getElement(),this._config.isAnimated)}}const Gi=".bs.focustrap",Ji=`focusin${Gi}`,Zi=`keydown.tab${Gi}`,tn="backward",en={autofocus:!0,trapElement:null},nn={autofocus:"boolean",trapElement:"element"};class sn extends H{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return en}static get DefaultType(){return nn}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),N.off(document,Gi),N.on(document,Ji,(t=>this._handleFocusin(t))),N.on(document,Zi,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,N.off(document,Gi))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=z.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===tn?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?tn:"forward")}}const on=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",rn=".sticky-top",an="padding-right",ln="margin-right";class cn{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,an,(e=>e+t)),this._setElementAttributes(on,an,(e=>e+t)),this._setElementAttributes(rn,ln,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,an),this._resetElementAttributes(on,an),this._resetElementAttributes(rn,ln)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&F.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=F.getDataAttribute(t,e);null!==i?(F.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(o(t))e(t);else for(const i of z.find(t,this._element))e(i)}}const hn=".bs.modal",dn=`hide${hn}`,un=`hidePrevented${hn}`,fn=`hidden${hn}`,pn=`show${hn}`,mn=`shown${hn}`,gn=`resize${hn}`,_n=`click.dismiss${hn}`,bn=`mousedown.dismiss${hn}`,vn=`keydown.dismiss${hn}`,yn=`click${hn}.data-api`,wn="modal-open",An="show",En="modal-static",Tn={backdrop:!0,focus:!0,keyboard:!0},Cn={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class On extends W{constructor(t,e){super(t,e),this._dialog=z.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new cn,this._addEventListeners()}static get Default(){return Tn}static get DefaultType(){return Cn}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||N.trigger(this._element,pn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(wn),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(N.trigger(this._element,dn).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(An),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){N.off(window,hn),N.off(this._dialog,hn),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Ui({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=z.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),d(this._element),this._element.classList.add(An),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,N.trigger(this._element,mn,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){N.on(this._element,vn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),N.on(window,gn,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),N.on(this._element,bn,(t=>{N.one(this._element,_n,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(wn),this._resetAdjustments(),this._scrollBar.reset(),N.trigger(this._element,fn)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(N.trigger(this._element,un).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(En)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(En),this._queueCallback((()=>{this._element.classList.remove(En),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=p()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=p()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=On.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}N.on(document,yn,'[data-bs-toggle="modal"]',(function(t){const e=z.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),N.one(e,pn,(t=>{t.defaultPrevented||N.one(e,fn,(()=>{a(this)&&this.focus()}))}));const i=z.findOne(".modal.show");i&&On.getInstance(i).hide(),On.getOrCreateInstance(e).toggle(this)})),R(On),m(On);const xn=".bs.offcanvas",kn=".data-api",Ln=`load${xn}${kn}`,Sn="show",Dn="showing",$n="hiding",In=".offcanvas.show",Nn=`show${xn}`,Pn=`shown${xn}`,Mn=`hide${xn}`,jn=`hidePrevented${xn}`,Fn=`hidden${xn}`,Hn=`resize${xn}`,Wn=`click${xn}${kn}`,Bn=`keydown.dismiss${xn}`,zn={backdrop:!0,keyboard:!0,scroll:!1},Rn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class qn extends W{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return zn}static get DefaultType(){return Rn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||N.trigger(this._element,Nn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new cn).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Dn),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(Sn),this._element.classList.remove(Dn),N.trigger(this._element,Pn,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(N.trigger(this._element,Mn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add($n),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(Sn,$n),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new cn).reset(),N.trigger(this._element,Fn)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Ui({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():N.trigger(this._element,jn)}:null})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_addEventListeners(){N.on(this._element,Bn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():N.trigger(this._element,jn))}))}static jQueryInterface(t){return this.each((function(){const e=qn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}N.on(document,Wn,'[data-bs-toggle="offcanvas"]',(function(t){const e=z.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this))return;N.one(e,Fn,(()=>{a(this)&&this.focus()}));const i=z.findOne(In);i&&i!==e&&qn.getInstance(i).hide(),qn.getOrCreateInstance(e).toggle(this)})),N.on(window,Ln,(()=>{for(const t of z.find(In))qn.getOrCreateInstance(t).show()})),N.on(window,Hn,(()=>{for(const t of z.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&qn.getOrCreateInstance(t).hide()})),R(qn),m(qn);const Vn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Kn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Qn=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Xn=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Kn.has(i)||Boolean(Qn.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Yn={allowList:Vn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:""},Un={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Gn={entry:"(string|element|function|null)",selector:"(string|element)"};class Jn extends H{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Yn}static get DefaultType(){return Un}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Gn)}_setContent(t,e,i){const n=z.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?o(e)?this._putElementInTemplate(r(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Xn(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return g(t,[this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const Zn=new Set(["sanitize","allowList","sanitizeFn"]),ts="fade",es="show",is=".modal",ns="hide.bs.modal",ss="hover",os="focus",rs={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},as={allowList:Vn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'
',title:"",trigger:"hover focus"},ls={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class cs extends W{constructor(t,e){if(void 0===vi)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return as}static get DefaultType(){return ls}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),N.off(this._element.closest(is),ns,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=N.trigger(this._element,this.constructor.eventName("show")),e=(c(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),N.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.on(t,"mouseover",h);this._queueCallback((()=>{N.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!N.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.off(t,"mouseover",h);this._activeTrigger.click=!1,this._activeTrigger[os]=!1,this._activeTrigger[ss]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),N.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(ts,es),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(ts),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Jn({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{".tooltip-inner":this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ts)}_isShown(){return this.tip&&this.tip.classList.contains(es)}_createPopper(t){const e=g(this._config.placement,[this,t,this._element]),i=rs[e.toUpperCase()];return bi(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return g(t,[this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...g(this._config.popperConfig,[e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)N.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===ss?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===ss?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");N.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?os:ss]=!0,e._enter()})),N.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?os:ss]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},N.on(this._element.closest(is),ns,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=F.getDataAttributes(this._element);for(const t of Object.keys(e))Zn.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=cs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(cs);const hs={...cs.Default,content:"",offset:[0,8],placement:"right",template:'
+ An introduction to FastHTML from the ground up, with four complete examples
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
This tutorial provides an alternate introduction to FastHTML by building out example applications. We also illustrate how to use FastHTML foundations to create custom web apps. Finally, this document serves as minimal context for a LLM to turn it into a FastHTML assistant.
+
Let’s get started.
+
+
FastHTML Basics
+
FastHTML is just Python. You can install it with pip install python-fasthtml. Extensions/components built for it can likewise be distributed via PyPI or as simple Python files.
+
The core usage of FastHTML is to define routes, and then to define what to do at each route. This is similar to the FastAPI web framework (in fact we implemented much of the functionality to match the FastAPI usage examples), but where FastAPI focuses on returning JSON data to build APIs, FastHTML focuses on returning HTML data.
+
Here’s a simple FastHTML app that returns a “Hello, World” message:
To run this app, place it in a file, say app.py, and then run it with python app.py.
+
INFO: Will watch for changes in these directories: ['/home/jonathan/fasthtml-example']
+INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
+INFO: Started reloader process [871942] using WatchFiles
+INFO: Started server process [871945]
+INFO: Waiting for application startup.
+INFO: Application startup complete.
+
If you navigate to http://127.0.0.1:8000 in a browser, you’ll see your “Hello, World”. If you edit the app.py file and save it, the server will reload and you’ll see the updated message when you refresh the page in your browser.
+
+
+
Constructing HTML
+
Notice we wrote some HTML in the previous example. We don’t want to do that! Some web frameworks require that you learn HTML, CSS, JavaScript AND some templating language AND python. We want to do as much as possible with just one language. Fortunately, the Python module fastcore.xml has all we need for constructing HTML from Python, and FastHTML includes all the tags you need to get started. For example:
If that import * worries you, you can always import only the tags you need.
+
FastHTML is smart enough to know about fastcore.xml, and so you don’t need to use the to_xml function to convert your FT objects to HTML. You can just return them as you would any other Python object. For example, if we modify our previous example to use fastcore.xml, we can return an FT object directly:
For debugging, you can right-click on the rendered HTML in the browser and select “Inspect” to see the underlying HTML that was generated. There you’ll also find the ‘network’ tab, which shows you the requests that were made to render the page. Refresh and look for the request to 127.0.0.1 - and you’ll see it’s just a GET request to /, and the response body is the HTML you just returned.
+
+
+
+
+
+
+Live Reloading
+
+
+
+
You can also enable live reloading so you don’t have to manually refresh your browser to view updates.
+
+
+
You can also use Starlette’s TestClient to try it out in a notebook:
FastHTML wraps things in an Html tag if you don’t do it yourself (unless the request comes from htmx, in which case you get the element directly). See FT objects and HTML for more on creating custom components or adding HTML rendering to existing Python objects. To give the page a non-default title, return a Title before your main content:
We’ll use this pattern often in the examples to follow.
+
+
+
Defining Routes
+
The HTTP protocol defines a number of methods (‘verbs’) to send requests to a server. The most common are GET, POST, PUT, DELETE, and HEAD. We saw ‘GET’ in action before - when you navigate to a URL, you’re making a GET request to that URL. We can do different things on a route for different HTTP methods. For example:
+
@app.route("/", methods='get')
+def home():
+return H1('Hello, World')
+
+@app.route("/", methods=['post', 'put'])
+def post_or_put():
+return"got a POST or PUT request"
+
This says that when someone navigates to the root URL “/” (i.e. sends a GET request), they will see the big “Hello, World” heading. When someone submits a POST or PUT request to the same URL, the server should return the string “got a post or put request”.
+
+
+
+
+
+
+Test the POST request
+
+
+
+
You can test the POST request with curl -X POST http://127.0.0.1:8000 -d "some data". This sends some data to the server, you should see the response “got a post or put request” printed in the terminal.
+
+
+
There are a few other ways you can specify the route+method - FastHTML has .get, .post, etc. as shorthand for route(..., methods=['get']), etc.
+
+
@app.get("/")
+def my_function():
+return"Hello World from a GET request"
+
+
Or you can use the @rt decorator without a method but specify the method with the name of the function. For example:
+
+
rt = app.route
+
+@rt("/")
+def post():
+return"Hello World from a POST request"
+
+
+
client.post("/").text
+
+
'Hello World from a POST request'
+
+
+
You’re welcome to pick whichever style you prefer. Using routes lets you show different content on different pages - ‘/home’, ‘/about’ and so on. You can also respond differently to different kinds of requests to the same route, as shown above. You can also pass data via the route:
Plain HTML probably isn’t quite what you imagine when you visualize your beautiful web app. CSS is the go-to language for styling HTML. But again, we don’t want to learn extra languages unless we absolutely have to! Fortunately, there are ways to get much more visually appealing sites by relying on the hard work of others, using existing CSS libraries. One of our favourites is PicoCSS. A common way to add CSS files to web pages is to use a <link> tag inside your HTML header, like this:
+As per the pico docs, we put all of our content inside a <main> tag with a class of container:
+
+
+
+
+
+
+
+
+
+
+Returning Tuples
+
+
+
+
We’re returning a tuple here (a title and the main page). Returning a tuple, list, FT object, or an object with a __ft__ method tells FastHTML to turn the main body into a full HTML page that includes the headers (including the pico link and our custom css) which we passed in. This only occurs if the request isn’t from HTMX (for HTMX requests we need only return the rendered components).
+
+
+
You can check out the Pico examples page to see how different elements will look. If everything is working, the page should now render nice text with our custom font, and it should respect the user’s light/dark mode preferences too.
+
If you want to override the default styles or add more custom CSS, you can do so by adding a <style> tag to the headers as shown above. So you are allowed to write CSS to your heart’s content - we just want to make sure you don’t necessarily have to! Later on we’ll see examples using other component libraries and tailwind css to do more fancy styling things, along with tips to get an LLM to write all those fiddly bits so you don’t have to.
+
+
+
Web Page -> Web App
+
Showing content is all well and good, but we typically expect a bit more interactivity from something calling itself a web app! So, let’s add a few different pages, and use a form to let users add messages to a list:
+
+
app = FastHTML()
+messages = ["This is a message, which will get rendered as a paragraph"]
+
+@app.get("/")
+def home():
+return Main(H1('Messages'),
+*[P(msg) for msg in messages],
+ A("Link to Page 2 (to add messages)", href="/page2"))
+
+@app.get("/page2")
+def page2():
+return Main(P("Add a message with the form below:"),
+ Form(Input(type="text", name="data"),
+ Button("Submit"),
+ action="/", method="post"))
+
+@app.post("/")
+def add_message(data:str):
+ messages.append(data)
+return home()
+
+
We re-render the entire homepage to show the newly added message. This is fine, but modern web apps often don’t re-render the entire page, they just update a part of the page. In fact even very complicated applications are often implemented as ‘Single Page Apps’ (SPAs). This is where HTMX comes in.
+
+
+
HTMX
+
HTMX addresses some key limitations of HTML. In vanilla HTML, links can trigger a GET request to show a new page, and forms can send requests containing data to the server. A lot of ‘Web 1.0’ design revolved around ways to use these to do everything we wanted. But why should only some elements be allowed to trigger requests? And why should we refresh the entire page with the result each time one does? HTMX extends HTML to allow us to trigger requests from any element on all kinds of events, and to update a part of the page without refreshing the entire page. It’s a powerful tool for building modern web apps.
+
It does this by adding attributes to HTML tags to make them do things. For example, here’s a page with a counter and a button that increments it:
+
+
app = FastHTML()
+
+count =0
+
+@app.get("/")
+def home():
+return Title("Count Demo"), Main(
+ H1("Count Demo"),
+ P(f"Count is set to {count}", id="count"),
+ Button("Increment", hx_post="/increment", hx_target="#count", hx_swap="innerHTML")
+ )
+
+@app.post("/increment")
+def increment():
+print("incrementing")
+global count
+ count +=1
+returnf"Count is set to {count}"
+
+
The button triggers a POST request to /increment (since we set hx_post="/increment"), which increments the count and returns the new count. The hx_target attribute tells HTMX where to put the result. If no target is specified it replaces the element that triggered the request. The hx_swap attribute specifies how it adds the result to the page. Useful options are:
+
+
innerHTML: Replace the target element’s content with the result.
+
outerHTML: Replace the target element with the result.
+
beforebegin: Insert the result before the target element.
+
beforeend: Insert the result inside the target element, after its last child.
+
afterbegin: Insert the result inside the target element, before its first child.
+
afterend: Insert the result after the target element.
+
+
You can also use an hx_swap of delete to delete the target element regardless of response, or of none to do nothing.
+
By default, requests are triggered by the “natural” event of an element - click in the case of a button (and most other elements). You can also specify different triggers, along with various modifiers - see the HTMX docs for more.
+
This pattern of having elements trigger requests that modify or replace other elements is a key part of the HTMX philosophy. It takes a little getting used to, but once mastered it is extremely powerful.
+
+
Replacing Elements Besides the Target
+
Sometimes having a single target is not enough, and we’d like to specify some additional elements to update or remove. In these cases, returning elements with an id that matches the element to be replaced and hx_swap_oob='true' will replace those elements too. We’ll use this in the next example to clear an input field when we submit a form.
+
+
+
+
Full Example #1 - ToDo App
+
The canonical demo web app! A TODO list. Rather than create yet another variant for this tutorial, we recommend starting with this video tutorial from Jeremy:
+
+
+
+
+
We’ve made a number of variants of this app - so in addition to the version shown in the video you can browse this series of examples with increasing complexity, the heavily-commented “idiomatic” version here, and the example linked from the FastHTML homepage.
+
+
+
Full Example #2 - Image Generation App
+
Let’s create an image generation app. We’d like to wrap a text-to-image model in a nice UI, where the user can type in a prompt and see a generated image appear. We’ll use a model hosted by Replicate to actually generate the images. Let’s start with the homepage, with a form to submit prompts and a div to hold the generated images:
Submitting the form will trigger a POST request to /, so next we need to generate an image and add it to the list. One problem: generating images is slow! We’ll start the generation in a separate thread, but this now surfaces a different problem: we want to update the UI right away, but our image will only be ready a few seconds later. This is a common pattern - think about how often you see a loading spinner online. We need a way to return a temporary bit of UI which will eventually be replaced by the final image. Here’s how we might do this:
The form sends the prompt to the / route, which starts the generation in a separate thread then returns two things:
+
+
A generation preview element that will be added to the top of the gen-list div (since that is the target_id of the form which triggered the request)
+
An input field that will replace the form’s input field (that has the same id), using the hx_swap_oob=‘true’ trick. This clears the prompt field so the user can type another prompt.
+
+
The generation preview first returns a temporary “Generating…” message, which polls the /generations/{id} route every second. This is done by setting hx_post to the route and hx_trigger to ‘every 1s’. The /generations/{id} route returns the preview element every second until the image is ready, at which point it returns the final image. Since the final image replaces the temporary one (hx_swap=‘outerHTML’), the polling stops running and the generation preview is now complete.
+
This works nicely - the user can submit several prompts without having to wait for the first one to generate, and as the images become available they are added to the list. You can see the full code of this version here.
+
+
Again, with Style
+
The app is functional, but can be improved. The next version adds more stylish generation previews, lays out the images in a grid layout that is responsive to different screen sizes, and adds a database to track generations and make them persistent. The database part is very similar to the todo list example, so let’s just quickly look at how we add the nice grid layout. This is what the result looks like:
+
+
+
+
Step one was looking around for existing components. The Pico CSS library we’ve been using has a rudimentary grid but recommends using an alternative layout system. One of the options listed was Flexbox.
+
To use Flexbox you create a “row” with one or more elements. You can specify how wide things should be with a specific syntax in the class name. For example, col-xs-12 means a box that will take up 12 columns (out of 12 total) of the row on extra small screens, col-sm-6 means a column that will take up 6 columns of the row on small screens, and so on. So if you want four columns on large screens you would use col-lg-3 for each item (i.e. each item is using 3 columns out of 12).
+
<div class="row">
+<div class="col-xs-12">
+<div class="box">This takes up the full width</div>
+</div>
+</div>
+
This was non-intuitive to me. Thankfully ChatGPT et al know web stuff quite well, and we can also experiment in a notebook to test things out:
+
+
grid = Html(
+ Link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css", type="text/css"),
+ Div(
+ Div(Div("This takes up the full width", cls="box", style="background-color: #800000;"), cls="col-xs-12"),
+ Div(Div("This takes up half", cls="box", style="background-color: #008000;"), cls="col-xs-6"),
+ Div(Div("This takes up half", cls="box", style="background-color: #0000B0;"), cls="col-xs-6"),
+ cls="row", style="color: #fff;"
+ )
+)
+show(grid)
+
+
+
+
+
+
+
+
This takes up the full width
+
+
+
This takes up half
+
+
+
This takes up half
+
+
+
+
+
+
Aside: when in doubt with CSS stuff, add a background color or a border so you can see what’s happening!
+
Translating this into our app, we have a new homepage with a div (class="row") to store the generated images / previews, and a generation_preview function that returns boxes with the appropriate classes and styles to make them appear in the grid. I chose a layout with different numbers of columns for different screen sizes, but you could also just specify the col-xs class if you wanted the same layout on all devices.
+
gridlink = Link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css", type="text/css")
+app = FastHTML(hdrs=(picolink, gridlink))
+
+# Main page
+@app.get("/")
+def get():
+ inp = Input(id="new-prompt", name="prompt", placeholder="Enter a prompt")
+ add = Form(Group(inp, Button("Generate")), hx_post="/", target_id='gen-list', hx_swap="afterbegin")
+ gen_containers = [generation_preview(g) for g in gens(limit=10)] # Start with last 10
+ gen_list = Div(*gen_containers[::-1], id='gen-list', cls="row") # flexbox container: class = row
+return Title('Image Generation Demo'), Main(H1('Magic Image Generation'), add, gen_list, cls='container')
+
+# Show the image (if available) and prompt for a generation
+def generation_preview(g):
+ grid_cls ="box col-xs-12 col-sm-6 col-md-4 col-lg-3"
+ image_path =f"{g.folder}/{g.id}.png"
+if os.path.exists(image_path):
+return Div(Card(
+ Img(src=image_path, alt="Card image", cls="card-img-top"),
+ Div(P(B("Prompt: "), g.prompt, cls="card-text"),cls="card-body"),
+ ), id=f'gen-{g.id}', cls=grid_cls)
+return Div(f"Generating gen {g.id} with prompt {g.prompt}",
+id=f'gen-{g.id}', hx_get=f"/gens/{g.id}",
+ hx_trigger="every 2s", hx_swap="outerHTML", cls=grid_cls)
+
You can see the final result in main.py in the image_app_simple example directory, along with info on deploying it (tl;dr don’t!). We’ve also deployed a version that only shows your generations (tied to browser session) and has a credit system to save our bank accounts. You can access that here. Now for the next question: how do we keep track of different users?
+
+
+
Again, with Sessions
+
At the moment everyone sees all images! How do we keep some sort of unique identifier tied to a user? Before going all the way to setting up users, login pages etc., let’s look at a way to at least limit generations to the user’s session. You could do this manually with cookies. For convenience and security, fasthtml (via Starlette) has a special mechanism for storing small amounts of data in the user’s browser via the session argument to your route. This acts like a dictionary and you can set and get values from it. For example, here we look for a session_id key, and if it doesn’t exist we generate a new one:
Refresh the page a few times - you’ll notice that the session ID remains the same. If you clear your browsing data, you’ll get a new session ID. And if you load the page in a different browser (but not a different tab), you’ll get a new session ID. This will persist within the current browser, letting us use it as a key for our generations. As a bonus, someone can’t spoof this session id by passing it in another way (for example, sending a query parameter). Behind the scenes, the data is stored in a browser cookie but it is signed with a secret key that stops the user or anyone nefarious from being able to tamper with it. The cookie is decoded back into a dictionary by something called a middleware function, which we won’t cover here. All you need to know is that we can use this to store bits of state in the user’s browser.
+
In the image app example, we can add a session_id column to our database, and modify our homepage like so:
+
@app.get("/")
+def get(session):
+if'session_id'notin session: session['session_id'] =str(uuid.uuid4())
+ inp = Input(id="new-prompt", name="prompt", placeholder="Enter a prompt")
+ add = Form(Group(inp, Button("Generate")), hx_post="/", target_id='gen-list', hx_swap="afterbegin")
+ gen_containers = [generation_preview(g) for g in gens(limit=10, where=f"session_id == '{session['session_id']}'")]
+ ...
+
So we check if the session id exists in the session, add one if not, and then limit the generations shown to only those tied to this session id. We filter the database with a where clause - see [TODO link Jeremy’s example for a more reliable way to do this]. The only other change we need to make is to store the session id in the database when a generation is made. You can check out this version here. You could instead write this app without relying on a database at all - simply storing the filenames of the generated images in the session, for example. But this more general approach of linking some kind of unique session identifier to users or data in our tables is a useful general pattern for more complex examples.
+
+
+
Again, with Credits!
+
Generating images with replicate costs money. So next let’s add a pool of credits that get used up whenever anyone generates an image. To recover our lost funds, we’ll also set up a payment system so that generous users can buy more credits for everyone. You could modify this to let users buy credits tied to their session ID, but at that point you risk having angry customers losing their money after wiping their browser history, and should consider setting up proper account management :)
+
Taking payments with Stripe is intimidating but very doable. Here’s a tutorial that shows the general principle using Flask. As with other popular tasks in the web-dev world, ChatGPT knows a lot about Stripe - but you should exercise extra caution when writing code that handles money!
A way to create a Stripe checkout session and redirect the user to the session URL
+
‘Success’ and ‘Cancel’ routes to handle the result of the checkout
+
A route that listens for a webhook from Stripe to update the number of credits when a payment is made.
+
+
In a typical application you’ll want to keep track of which users make payments, catch other kinds of stripe events and so on. This example is more a ‘this is possible, do your own research’ than ‘this is how you do it’. But hopefully it does illustrate the key idea: there is no magic here. Stripe (and many other technologies) relies on sending users to different routes and shuttling data back and forth in requests. And we know how to do that!
+
+
+
+
More on Routing and Request Parameters
+
There are a number of ways information can be passed to the server. When you specify arguments to a route, FastHTML will search the request for values with the same name, and convert them to the correct type. In order, it searches
+
+
The path parameters
+
The query parameters
+
The cookies
+
The headers
+
The session
+
Form data
+
+
There are also a few special arguments
+
+
request (or any prefix like req): gets the raw Starlette Request object
+
session (or any prefix like sess): gets the session object
+
auth
+
htmx
+
app
+
+
In this section let’s quickly look at some of these in action.
+
+
app = FastHTML()
+cli = TestClient(app)
+
+
Part of the route (path parameters):
+
+
@app.get('/user/{nm}')
+def _(nm:str): returnf"Good day to you, {nm}!"
+
+cli.get('/user/jph').text
@app.get("/datie/")
+def datie(d:parsed_date): return d
+
+date_str ="17th of May, 2024, 2p"
+print(cli.get(f'/datie/?d={date_str}').text)
+
+
2024-05-17 14:00:00
+
+
+
Matching a dataclass:
+
+
from dataclasses import dataclass, asdict
+
+@dataclass
+class Bodie:
+ a:int;b:str
+
+@app.route("/bodie/{nm}")
+def post(nm:str, data:Bodie):
+ res = asdict(data)
+ res['nm'] = nm
+return res
+
+cli.post('/bodie/me', data=dict(a=1, b='foo')).text
+
+
'{"a":1,"b":"foo","nm":"me"}'
+
+
+
+
Cookies
+
Cookies can be set via a Starlette Response object, and can be read back by specifying the name:
+
+
from datetime import datetime
+
+@app.get("/setcookie")
+def setc(req):
+ now = datetime.now()
+ res = Response(f'Set to {now}')
+ res.set_cookie('now', str(now))
+return res
+
+cli.get('/setcookie').text
+
+
'Set to 2024-07-20 23:14:54.364793'
+
+
+
+
@app.get("/getcookie")
+def getc(now:parsed_date): returnf'Cookie was set at time {now.time()}'
+
+cli.get('/getcookie').text
+
+
'Cookie was set at time 23:14:54.364793'
+
+
+
+
+
User Agent and HX-Request
+
An argument of user_agent will match the header User-Agent. This holds for special headers like HX-Request (used by HTMX to signal when a request comes from an HTMX request) - the general pattern is that “-” is replaced with “_” and strings are turned to lowercase.
If you add an argument called request(or any prefix of that, for example req) it will be populated with the Starlette Request object. This is useful if you want to do your own processing manually. For example, although FastHTML will parse forms for you, you could instead get form data like so:
We used this to set cookies in the previous example. See the Starlette docs for more information on the Response object.
+
+
+
Static Files
+
We often want to serve static files like images. This is easily done! For common file types (images, CSS etc) we can create a route that returns a Starlette FileResponse like so:
+
# For images, CSS, etc.
+@app.get("/{fname:path}.{ext:static}")
+def static(fname: str, ext: str):
+return FileResponse(f'{fname}.{ext}')
+
You can customize it to suit your needs (for example, only serving files in a certain directory). You’ll notice some variant of this route in all our complete examples - even for apps with no static files the browser will typically request a /favicon.ico file, for example, and as the astute among you will have noticed this has sparked a bit of competition between Johno and Jeremy regarding which country flag should serve as the default!
+
+
+
WebSockets
+
For certain applications such as multiplayer games, websockets can be a powerful feature. Luckily HTMX and FastHTML has you covered! Simply specify that you wish to include the websocket header extension from HTMX:
+
app = FastHTML(ws_hdr=True)
+rt = app.route
+
With that, you are now able to specify the different websocket specific HTMX goodies. For example, say we have a website we want to setup a websocket, you can simply:
And this will setup a connection on the route /ws along with a form that will send a message to the websocket whenever the form is submitted. Let’s go ahead and handle this route:
One thing you might have noticed is a lack of target id for our websocket trigger for swapping HTML content. This is because HTMX always swaps content with websockets with Out of Band Swaps. Therefore, HTMX will look for the id in the returned HTML content from the server for determining what to swap. To send stuff to the client, you can either use the send parameter or simply return the content or both!
+
Now, sometimes you might want to perform actions when a client connects or disconnects such as add or remove a user from a player queue. To hook into these events, you can pass your connection or disconnection function to the app.ws decorator:
Full Example #3 - Chatbot Example with DaisyUI Components
+
Let’s go back to the topic of adding components or styling beyond the simple PicoCSS examples so far. How might we adopt a component or framework? In this example, let’s build a chatbot UI leveraging the DaisyUI chat bubble. The final result will look like this:
+
+
+
+
At first glance, DaisyUI’s chat component looks quite intimidating. The examples look like this:
ChatGPT knows DaisyUI and Tailwind (DaisyUI is a Tailwind component library)
+
We can build things up piece by piece with AI standing by to help.
+
+
https://h2f.answer.ai/ is a tool that can convert HTML to FT (fastcore.xml) and back, which is useful for getting a quick starting point when you have an HTML example to start from.
+
We can strip out some unnecessary bits and try to get the simplest possible example working in a notebook first:
Now we can extend this to render multiple messages, with the message being on the left (chat-start) or right (chat-end) depending on the role. While we’re at it, we can also change the color (chat-bubble-primary) of the message and put them all in a chat-box div:
+
+
messages = [
+ {"role":"user", "content":"Hello"},
+ {"role":"assistant", "content":"Hi, how can I assist you?"}
+]
+
+def ChatMessage(msg):
+return Div(
+ Div(msg['role'], cls="chat-header"),
+ Div(msg['content'], cls=f"chat-bubble chat-bubble-{'primary'if msg['role'] =='user'else'secondary'}"),
+ cls=f"chat chat-{'end'if msg['role'] =='user'else'start'}")
+
+chatbox = Div(*[ChatMessage(msg) for msg in messages], cls="chat-box", id="chatlist")
+
+# show(Html(*headers, chatbox)) # Uncomment to view
+
+
Next, it was back to the ChatGPT to tweak the chat box so it wouldn’t grow as messages were added. I asked:
+
"I have something like this (it's working now)
+[code]
+The messages are added to this div so it grows over time.
+Is there a way I can set it's height to always be 80% of the total window height with a scroll bar if needed?"
+
Based on this query GPT4o helpfully shared that “This can be achieved using Tailwind CSS utility classes. Specifically, you can use h-[80vh] to set the height to 80% of the viewport height, and overflow-y-auto to add a vertical scroll bar when needed.”
+
To put it another way: none of the CSS classes in the following example were written by a human, and what edits I did make were informed by advice from the AI that made it relatively painless!
+
The actual chat functionality of the app is based on our claudette library. As with the image example, we face a potential hiccup in that getting a response from an LLM is slow. We need a way to have the user message added to the UI immediately, and then have the response added once it’s available. We could do something similar to the image generation example above, or use websockets. Check out the full example for implementations of both, along with further details.
+
+
+
Full Example #4 - Multiplayer Game of Life Example with Websockets
+
Let’s see how we can implement a collaborative website using Websockets in FastHTML. To showcase this, we will use the famous Conway’s Game of Life, which is a game that takes place in a grid world. Each cell in the grid can be either alive or dead. The cell’s state is initially given by a user before the game is started and then evolves through the iteration of the grid world once the clock starts. Whether a cell’s state will change from the previous state depends on simple rules based on its neighboring cells’ states. Here is the standard Game of Life logic implemented in Python courtesy of ChatGPT:
This would be a very dull game if we were to run it, since the initial state of everything would remain dead. Therefore, we need a way of letting the user give an initial state before starting the game. FastHTML to the rescue!
Above is a component for representing the game’s state that the user can interact with and update on the server using cool HTMX features such as hx_vals for determining which cell was clicked to make it dead or alive. Now, you probably noticed that the HTTP request in this case is a PUT request, which does not return anything and this means our client’s view of the grid world and the server’s game state will immediately become out of sync :(. We could of course just return a new Grid component with the updated state, but that would only work for a single client, if we had more, they quickly get out of sync with each other and the server. Now Websockets to the rescue!
+
Websockets are a way for the server to keep a persistent connection with clients and send data to the client without explicitly being requested for information, which is not possible with HTTP. Luckily FastHTML and HTMX work well with Websockets. Simply state you wish to use websockets for your app and define a websocket route:
Here we simply keep track of all the players that have connected or disconnected to our site and when an update occurs, we send updates to all the players still connected via websockets. Via HTMX, you are still simply exchanging HTML from the server to the client and will swap in the content based on how you setup your hx_swap attribute. There is only one difference, that being all swaps are OOB. You can find more information on the HTMX websocket extension documentation page here. You can find a full fledge hosted example of this app here.
+
+
+
FT objects and HTML
+
These FT objects create a ‘FastTag’ structure [tag,children,attrs] for to_xml(). When we call Div(...), the elements we pass in are the children. Attributes are passed in as keywords. class and for are special words in python, so we use cls, klass or _class instead of class and fr or _for instead of for. Note these objects are just 3-element lists - you can create custom ones too as long as they’re also 3-element lists. Alternately, leaf nodes can be strings instead (which is why you can do Div('some text')). If you pass something that isn’t a 3-element list or a string, it will be converted to a string using str()… unless (our final trick) you define a __ft__ method that will run before str(), so you can render things a custom way.
+
For example, here’s one way we could make a custom class that can be rendered into HTML:
+
+
class Person:
+def__init__(self, name, age):
+self.name = name
+self.age = age
+
+def __ft__(self):
+return ['div', [f'{self.name} is {self.age} years old.'], {}]
+
+p = Person('Jonathan', 28)
+print(to_xml(Div(p, "more text", cls="container")))
+
+
<div class="container">
+ <div>Jonathan is 28 years old.</div>
+more text
+</div>
+
+
+
+
In the examples, you’ll see we often patch in __ft__ methods to existing classes to control how they’re rendered. For example, if Person didn’t have a __ft__ method or we wanted to override it, we could add a new one like this:
Some tags from fastcore.xml are overwritten by fasthtml.core and a few are further extended by fasthtml.xtend using this method. Over time, we hope to see others developing custom components too, giving us a larger and larger ecosystem of reusable components.
+
+
+
Custom Scripts and Styling
+
There are many popular JavaScript and CSS libraries that can be used via a simple Script or Style tag. But in some cases you will need to write more custom code. FastHTML’s js.py contains a few examples that may be useful as reference.
+
For example, to use the marked.js library to render markdown in a div, including in components added after the page has loaded via htmx, we do something like this:
+
import { marked } from"https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
+proc_htmx('%s', e => e.innerHTML= marked.parse(e.textContent));
+
proc_htmx is a shortcut that we wrote to apply a function to elements matching a selector, including the element that triggered the event. Here’s the code for reference:
The AI Pictionary example uses a larger chunk of custom JavaScript to handle the drawing canvas. It’s a good example of the type of application where running code on the client side makes the most sense, but still shows how you can integrate it with FastHTML on the server side to add functionality (like the AI responses) easily.
+
Adding styling with custom CSS and libraries such as tailwind is done the same way we add custom JavaScript. The doodle example uses Doodle.CSS to style the page in a quirky way.
+
+
+
Deploying Your App
+
We can deploy FastHTML almost anywhere you can deploy python apps. We’ve tested Railway, Replit, HuggingFace, and PythonAnywhere.
Use the fh_railway_deploy script to deploy our project:
+
+
fh_railway_deploy MY_APP_NAME
+
What the script does for us:
+
+
Do we have an existing railway project?
+
+
Yes: Link the project folder to our existing Railway project.
+
No: Create a new Railway project.
+
+
Deploy the project. We’ll see the logs as the service is built and run!
+
Fetches and displays the URL of our app.
+
By default, mounts a /app/data folder on the cloud to our app’s root folder. The app is run in /app by default, so from our app anything we store in /data will persist across restarts.
+
+
A final note about Railway: We can add secrets like API keys that can be accessed as environment variables from our apps via ‘Variables’. For example, for the image app (TODO link), we can add a REPLICATE_API_KEY variable, and then in main.py we can access it as os.environ['REPLICATE_API_KEY'].
+
+
+
Replit
+
Fork this repl for a minimal example you can edit to your heart’s content. .replit has been edited to add the right run command (run = ["uvicorn", "main:app", "--reload"]) and to set up the ports correctly. FastHTML was installed with poetry add python-fasthtml, you can add additional packages as needed in the same way. Running the app in Replit will show you a webview, but you may need to open in a new tab for all features (such as cookies) to work. When you’re ready, you can deploy your app by clicking the ‘Deploy’ button. You pay for usage - for an app that is mostly idle the cost is usually a few cents per month.
+
You can store secrets like API keys via the ‘Secrets’ tab in the Replit project settings.
+
+
+
HuggingFace
+
Follow the instructions in this repository to deploy to HuggingFace spaces.
+
+
+
+
Where Next?
+
We’ve covered a lot of ground here! Hopefully this has given you plenty to work with in building your own FastHTML apps. If you have any questions, feel free to ask in the #fasthtml Discord channel (in the fastai Discord community). You can look through the other examples in the fasthtml-example repository for more ideas, and keep an eye on Jeremy’s YouTube channel where we’ll be releasing a number of “dev chats” related to FastHTML in the near future.
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tutorials/by_example.html.md b/tutorials/by_example.html.md
new file mode 100644
index 00000000..f34c3f29
--- /dev/null
+++ b/tutorials/by_example.html.md
@@ -0,0 +1,1558 @@
+# FastHTML By Example
+
+
+
+
+This tutorial provides an alternate introduction to FastHTML by building
+out example applications. We also illustrate how to use FastHTML
+foundations to create custom web apps. Finally, this document serves as
+minimal context for a LLM to turn it into a FastHTML assistant.
+
+Let’s get started.
+
+## FastHTML Basics
+
+FastHTML is *just Python*. You can install it with
+`pip install python-fasthtml`. Extensions/components built for it can
+likewise be distributed via PyPI or as simple Python files.
+
+The core usage of FastHTML is to define routes, and then to define what
+to do at each route. This is similar to the
+[FastAPI](https://fastapi.tiangolo.com/) web framework (in fact we
+implemented much of the functionality to match the FastAPI usage
+examples), but where FastAPI focuses on returning JSON data to build
+APIs, FastHTML focuses on returning HTML data.
+
+Here’s a simple FastHTML app that returns a “Hello, World” message:
+
+``` python
+from fasthtml.common import FastHTML, serve
+
+app = FastHTML()
+
+@app.get("/")
+def home():
+ return "
Hello, World
"
+
+serve()
+```
+
+To run this app, place it in a file, say `app.py`, and then run it with
+`python app.py`.
+
+ INFO: Will watch for changes in these directories: ['/home/jonathan/fasthtml-example']
+ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
+ INFO: Started reloader process [871942] using WatchFiles
+ INFO: Started server process [871945]
+ INFO: Waiting for application startup.
+ INFO: Application startup complete.
+
+If you navigate to in a browser, you’ll see your
+“Hello, World”. If you edit the `app.py` file and save it, the server
+will reload and you’ll see the updated message when you refresh the page
+in your browser.
+
+## Constructing HTML
+
+Notice we wrote some HTML in the previous example. We don’t want to do
+that! Some web frameworks require that you learn HTML, CSS, JavaScript
+AND some templating language AND python. We want to do as much as
+possible with just one language. Fortunately, the Python module
+[fastcore.xml](https://fastcore.fast.ai/xml.html) has all we need for
+constructing HTML from Python, and FastHTML includes all the tags you
+need to get started. For example:
+
+``` python
+from fasthtml.common import *
+page = Html(
+ Head(Title('Some page')),
+ Body(Div('Some text, ', A('A link', href='https://example.com'), Img(src="https://placehold.co/200"), cls='myclass')))
+print(to_xml(page))
+```
+
+ !doctype>
+
+
+
+ Some page
+
+
+
+
+
+
+If that `import *` worries you, you can always import only the tags you
+need.
+
+FastHTML is smart enough to know about fastcore.xml, and so you don’t
+need to use the `to_xml` function to convert your FT objects to HTML.
+You can just return them as you would any other Python object. For
+example, if we modify our previous example to use fastcore.xml, we can
+return an FT object directly:
+
+``` python
+from fasthtml.common import *
+app = FastHTML()
+
+@app.get("/")
+def home():
+ page = Html(
+ Head(Title('Some page')),
+ Body(Div('Some text, ', A('A link', href='https://example.com'), Img(src="https://placehold.co/200"), cls='myclass')))
+ return page
+
+serve()
+```
+
+This will render the HTML in the browser.
+
+For debugging, you can right-click on the rendered HTML in the browser
+and select “Inspect” to see the underlying HTML that was generated.
+There you’ll also find the ‘network’ tab, which shows you the requests
+that were made to render the page. Refresh and look for the request to
+`127.0.0.1` - and you’ll see it’s just a `GET` request to `/`, and the
+response body is the HTML you just returned.
+
+
+
+> **Live Reloading**
+>
+> You can also enable [live reloading](../ref/live_reload.ipynb) so you
+> don’t have to manually refresh your browser to view updates.
+
+
+
+You can also use Starlette’s `TestClient` to try it out in a notebook:
+
+``` python
+from starlette.testclient import TestClient
+client = TestClient(app)
+r = client.get("/")
+print(r.text)
+```
+
+
+ Some page
+
+
+
+
+
+FastHTML wraps things in an Html tag if you don’t do it yourself (unless
+the request comes from htmx, in which case you get the element
+directly). See [FT objects and HTML](#ft-objects-and-html) for more on
+creating custom components or adding HTML rendering to existing Python
+objects. To give the page a non-default title, return a Title before
+your main content:
+
+``` python
+app = FastHTML()
+
+@app.get("/")
+def home():
+ return Title("Page Demo"), Div(H1('Hello, World'), P('Some text'), P('Some more text'))
+
+client = TestClient(app)
+print(client.get("/").text)
+```
+
+ !doctype>
+
+
+
+ Page Demo
+
+
+
+
+
+
+
+
+
Hello, World
+
Some text
+
Some more text
+
+
+
+
+We’ll use this pattern often in the examples to follow.
+
+## Defining Routes
+
+The HTTP protocol defines a number of methods (‘verbs’) to send requests
+to a server. The most common are GET, POST, PUT, DELETE, and HEAD. We
+saw ‘GET’ in action before - when you navigate to a URL, you’re making a
+GET request to that URL. We can do different things on a route for
+different HTTP methods. For example:
+
+``` python
+@app.route("/", methods='get')
+def home():
+ return H1('Hello, World')
+
+@app.route("/", methods=['post', 'put'])
+def post_or_put():
+ return "got a POST or PUT request"
+```
+
+This says that when someone navigates to the root URL “/” (i.e. sends a
+GET request), they will see the big “Hello, World” heading. When someone
+submits a POST or PUT request to the same URL, the server should return
+the string “got a post or put request”.
+
+
+
+> **Test the POST request**
+>
+> You can test the POST request with
+> `curl -X POST http://127.0.0.1:8000 -d "some data"`. This sends some
+> data to the server, you should see the response “got a post or put
+> request” printed in the terminal.
+
+
+
+There are a few other ways you can specify the route+method - FastHTML
+has `.get`, `.post`, etc. as shorthand for
+`route(..., methods=['get'])`, etc.
+
+``` python
+@app.get("/")
+def my_function():
+ return "Hello World from a GET request"
+```
+
+Or you can use the `@rt` decorator without a method but specify the
+method with the name of the function. For example:
+
+``` python
+rt = app.route
+
+@rt("/")
+def post():
+ return "Hello World from a POST request"
+```
+
+``` python
+client.post("/").text
+```
+
+ 'Hello World from a POST request'
+
+You’re welcome to pick whichever style you prefer. Using routes lets you
+show different content on different pages - ‘/home’, ‘/about’ and so on.
+You can also respond differently to different kinds of requests to the
+same route, as shown above. You can also pass data via the route:
+
+
+
+## `@app.get`
+
+``` python
+@app.get("/greet/{nm}")
+def greet(nm:str):
+ return f"Good day to you, {nm}!"
+
+client.get("/greet/Dave").text
+```
+
+ 'Good day to you, Dave!'
+
+## `@rt`
+
+``` python
+@rt("/greet/{nm}")
+def get(nm:str):
+ return f"Good day to you, {nm}!"
+
+client.get("/greet/Dave").text
+```
+
+ 'Good day to you, Dave!'
+
+
+
+More on this in the [More on Routing and Request
+Parameters](#more-on-routing-and-request-parameters) section, which goes
+deeper into the different ways to get information from a request.
+
+## Styling Basics
+
+Plain HTML probably isn’t quite what you imagine when you visualize your
+beautiful web app. CSS is the go-to language for styling HTML. But
+again, we don’t want to learn extra languages unless we absolutely have
+to! Fortunately, there are ways to get much more visually appealing
+sites by relying on the hard work of others, using existing CSS
+libraries. One of our favourites is [PicoCSS](https://picocss.com/). A
+common way to add CSS files to web pages is to use a
+[``](https://www.w3schools.com/tags/tag_link.asp) tag inside your
+[HTML header](https://www.w3schools.com/tags/tag_header.asp), like this:
+
+``` html
+
+ ...
+
+
+```
+
+For convenience, FastHTML already defines a Pico component for you with
+`picolink`:
+
+``` python
+print(to_xml(picolink))
+```
+
+
+
+
+
+
+ How to build a website with custom JavaScript in FastHTML step-by-step
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Installation
+
You’ll need the following software to complete the tutorial, read on for specific installation instructions:
+
+
Python
+
A Python package manager such as pip (which normally comes with Python) or uv
+
FastHTML
+
Web browser
+
Railway.app account
+
+
If you haven’t worked with Python before, we recommend getting started with Miniconda.
+
Note that you will only need to follow the steps in the installation section once per environment. If you create a new repo, you won’t need to redo these.
+
+
Install FastHTML
+
For Mac, Windows and Linux, enter:
+
pip install python-fasthtml
+
+
+
+
First steps
+
By the end of this section you’ll have your own FastHTML website with tests deployed to railway.app.
+
+
Create a hello world
+
Create a new folder to organize all the files for your project. Inside this folder, create a file called main.py and add the following code to it:
Finally, run python main.py in your terminal and open your browser to the ‘Link’ that appears.
+
+
+
QuickDraw: A FastHTML Adventure 🎨✨
+
The end result of this tutorial will be QuickDraw, a real-time collaborative drawing app using FastHTML. Here is what the final site will look like:
+
+
+
+
+
Drawing Rooms
+
Drawing rooms are the core concept of our application. Each room represents a separate drawing space where a user can let their inner Picasso shine. Here’s a detailed breakdown:
We are specifying a render function to convert our dataclass into HTML, which is the same as extending the __ft__ method from the patch decorator we used before. We will use this method for the rest of the tutorial since it is a lot cleaner and easier to read.
+
+
We’re using a SQLite database (via FastLite) to store our rooms.
+
Each room has an id (integer), a name (string), and a created_at timestamp (string).
+
The Room dataclass is automatically generated based on this structure.
+
+
+
Creating a room
+
+
+
+
main.py
+
+
@rt("/")
+def get():
+# The 'Input' id defaults to the same as the name, so you can omit it if you wish
+ create_room = Form(Input(id="name", name="name", placeholder="New Room Name"),
+ Button("Create Room"),
+ hx_post="/rooms", hx_target="#rooms-list", hx_swap="afterbegin")
+ rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list')
+return Titled("DrawCollab",
+ H1("DrawCollab"),
+ create_room, rooms_list)
+
+@rt("/rooms")
+asyncdef post(room:Room):
+ room.created_at = datetime.now().isoformat()
+return rooms.insert(room)
+
+
+
When a user submits the “Create Room” form, this route is called.
+
It creates a new Room object, sets the creation time, and inserts it into the database.
+
It returns an HTML list item with a link to the new room, which is dynamically added to the room list on the homepage thanks to HTMX.
Now run python main.py in your terminal and open your browser to the ‘Link’ that appears. You should see a page with a form to create a new room and a list of existing rooms.
+
+
+
The Canvas - Let’s Get Drawing! 🖌️
+
Time to add the actual drawing functionality. We’ll use Fabric.js for this:
Now we’ve got a drawing canvas! FastHTML makes it easy to include external libraries and add custom JavaScript.
+
+
+
Saving and Loading Canvases 💾
+
Now that we have a working drawing canvas, let’s add the ability to save and load drawings. We’ll modify our database schema to include a canvas_data field, and add new routes for saving and loading canvas data. Here’s how we’ll update our code:
With these changes, users can now save their drawings and load them when they return to the room. The canvas data is stored as a JSON string in the database, allowing for easy serialization and deserialization. Try it out! Create a new room, make a drawing, save it, and then reload the page. You should see your drawing reappear, ready for further editing.
You can deploy your website to a number of hosting providers, for this tutorial we’ll be using Railway. To get started, make sure you create an account and install the Railway CLI. Once installed, make sure to run railway login to log in to your account.
+
To make deploying your website as easy as possible, FastHTMl comes with a built in CLI tool that will handle most of the deployment process for you. To deploy your website, run the following command in your terminal in the root directory of your project:
+
fh_railway_deploy quickdraw
+
+
+
+
+
+
+Note
+
+
+
+
Your app must be located in a main.py file for this to work.
+
+
+
+
+
Conclusion: You’re a FastHTML Artist Now! 🎨🚀
+
Congratulations! You’ve just built a sleek, interactive web application using FastHTML. Let’s recap what we’ve learned:
+
+
FastHTML allows you to create dynamic web apps with minimal code.
+
We used FastHTML’s routing system to handle different pages and actions.
+
We integrated with a SQLite database to store room information and canvas data.
+
We utilized Fabric.js to create an interactive drawing canvas.
+
We implemented features like color picking, brush size adjustment, and canvas saving.
+
We used HTMX for seamless, partial page updates without full reloads.
+
We learned how to deploy our FastHTML application to Railway for easy hosting.
+
+
You’ve taken your first steps into the world of FastHTML development. From here, the possibilities are endless! You could enhance the drawing app further by adding features like:
+
+
Implementing different drawing tools (e.g., shapes, text)
+
Adding user authentication
+
Creating a gallery of saved drawings
+
Implementing real-time collaborative drawing using WebSockets
+
+
Whatever you choose to build next, FastHTML has got your back. Now go forth and create something awesome! Happy coding! 🖼️🚀
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tutorials/e2e.html.md b/tutorials/e2e.html.md
new file mode 100644
index 00000000..99fc4d0c
--- /dev/null
+++ b/tutorials/e2e.html.md
@@ -0,0 +1,501 @@
+# JS App Walkthrough
+
+
+
+
+## Installation
+
+You’ll need the following software to complete the tutorial, read on for
+specific installation instructions:
+
+1. Python
+2. A Python package manager such as pip (which normally comes with
+ Python) or uv
+3. FastHTML
+4. Web browser
+5. Railway.app account
+
+If you haven’t worked with Python before, we recommend getting started
+with [Miniconda](https://docs.anaconda.com/miniconda/).
+
+Note that you will only need to follow the steps in the installation
+section once per environment. If you create a new repo, you won’t need
+to redo these.
+
+### Install FastHTML
+
+For Mac, Windows and Linux, enter:
+
+``` sh
+pip install python-fasthtml
+```
+
+## First steps
+
+By the end of this section you’ll have your own FastHTML website with
+tests deployed to railway.app.
+
+### Create a hello world
+
+Create a new folder to organize all the files for your project. Inside
+this folder, create a file called `main.py` and add the following code
+to it:
+
+
+
+Finally, run `python main.py` in your terminal and open your browser to
+the ‘Link’ that appears.
+
+### QuickDraw: A FastHTML Adventure 🎨✨
+
+The end result of this tutorial will be QuickDraw, a real-time
+collaborative drawing app using FastHTML. Here is what the final site
+will look like:
+
+
+
+#### Drawing Rooms
+
+Drawing rooms are the core concept of our application. Each room
+represents a separate drawing space where a user can let their inner
+Picasso shine. Here’s a detailed breakdown:
+
+1. Room Creation and Storage
+
+
+
+Or you can use our
+[`fast_app`](https://AnswerDotAI.github.io/fasthtml/api/fastapp.html#fast_app)
+function to create a FastHTML app with a SQLite database and dataclass
+in one line:
+
+
+
+We are specifying a render function to convert our dataclass into HTML,
+which is the same as extending the `__ft__` method from the `patch`
+decorator we used before. We will use this method for the rest of the
+tutorial since it is a lot cleaner and easier to read.
+
+- We’re using a SQLite database (via FastLite) to store our rooms.
+- Each room has an id (integer), a name (string), and a created_at
+ timestamp (string).
+- The Room dataclass is automatically generated based on this structure.
+
+2. Creating a room
+
+
+
+**main.py**
+
+``` python
+@rt("/")
+def get():
+ # The 'Input' id defaults to the same as the name, so you can omit it if you wish
+ create_room = Form(Input(id="name", name="name", placeholder="New Room Name"),
+ Button("Create Room"),
+ hx_post="/rooms", hx_target="#rooms-list", hx_swap="afterbegin")
+ rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list')
+ return Titled("DrawCollab",
+ H1("DrawCollab"),
+ create_room, rooms_list)
+
+@rt("/rooms")
+async def post(room:Room):
+ room.created_at = datetime.now().isoformat()
+ return rooms.insert(room)
+```
+
+
+
+- When a user submits the “Create Room” form, this route is called.
+- It creates a new Room object, sets the creation time, and inserts it
+ into the database.
+- It returns an HTML list item with a link to the new room, which is
+ dynamically added to the room list on the homepage thanks to HTMX.
+
+3. Let’s give our rooms shape
+
+
+
+- This route renders the interface for a specific room.
+- It fetches the room from the database and renders a title, heading,
+ and paragraph.
+
+Here is the full code so far:
+
+
+
+Now run `python main.py` in your terminal and open your browser to the
+‘Link’ that appears. You should see a page with a form to create a new
+room and a list of existing rooms.
+
+#### The Canvas - Let’s Get Drawing! 🖌️
+
+Time to add the actual drawing functionality. We’ll use Fabric.js for
+this:
+
+
+
+Now we’ve got a drawing canvas! FastHTML makes it easy to include
+external libraries and add custom JavaScript.
+
+#### Saving and Loading Canvases 💾
+
+Now that we have a working drawing canvas, let’s add the ability to save
+and load drawings. We’ll modify our database schema to include a
+`canvas_data` field, and add new routes for saving and loading canvas
+data. Here’s how we’ll update our code:
+
+1. Modify the database schema:
+
+
+
+With these changes, users can now save their drawings and load them when
+they return to the room. The canvas data is stored as a JSON string in
+the database, allowing for easy serialization and deserialization. Try
+it out! Create a new room, make a drawing, save it, and then reload the
+page. You should see your drawing reappear, ready for further editing.
+
+Here is the completed code:
+
+
+
+### Deploying to Railway
+
+You can deploy your website to a number of hosting providers, for this
+tutorial we’ll be using Railway. To get started, make sure you create an
+[account](https://railway.app/) and install the [Railway
+CLI](https://docs.railway.app/guides/cli). Once installed, make sure to
+run `railway login` to log in to your account.
+
+To make deploying your website as easy as possible, FastHTMl comes with
+a built in CLI tool that will handle most of the deployment process for
+you. To deploy your website, run the following command in your terminal
+in the root directory of your project:
+
+``` sh
+fh_railway_deploy quickdraw
+```
+
+
+
+> **Note**
+>
+> Your app must be located in a `main.py` file for this to work.
+
+
+
+### Conclusion: You’re a FastHTML Artist Now! 🎨🚀
+
+Congratulations! You’ve just built a sleek, interactive web application
+using FastHTML. Let’s recap what we’ve learned:
+
+1. FastHTML allows you to create dynamic web apps with minimal code.
+2. We used FastHTML’s routing system to handle different pages and
+ actions.
+3. We integrated with a SQLite database to store room information and
+ canvas data.
+4. We utilized Fabric.js to create an interactive drawing canvas.
+5. We implemented features like color picking, brush size adjustment,
+ and canvas saving.
+6. We used HTMX for seamless, partial page updates without full
+ reloads.
+7. We learned how to deploy our FastHTML application to Railway for
+ easy hosting.
+
+You’ve taken your first steps into the world of FastHTML development.
+From here, the possibilities are endless! You could enhance the drawing
+app further by adding features like:
+
+- Implementing different drawing tools (e.g., shapes, text)
+- Adding user authentication
+- Creating a gallery of saved drawings
+- Implementing real-time collaborative drawing using WebSockets
+
+Whatever you choose to build next, FastHTML has got your back. Now go
+forth and create something awesome! Happy coding! 🖼️🚀
diff --git a/tutorials/imgs/quickdraw.png b/tutorials/imgs/quickdraw.png
new file mode 100644
index 00000000..37c8a7cd
Binary files /dev/null and b/tutorials/imgs/quickdraw.png differ
diff --git a/tutorials/index.html b/tutorials/index.html
new file mode 100644
index 00000000..fe97a0ca
--- /dev/null
+++ b/tutorials/index.html
@@ -0,0 +1,903 @@
+
+
+
+
+
+
+
+
+
+Tutorials – fasthtml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Writing FastHTML applications in Jupyter notebooks requires a slightly different process than normal Python applications.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Writing FastHTML applications in Jupyter notebooks requires a slightly different process than normal Python applications.
+
+
+
+
+
+
+Download this notebook and try it yourself
+
+
+
+
The source code for this page is a Jupyter notebook. That makes it easy to directly experiment with it. However, as this is working code that means we have to comment out a few things in order for the documentation to build.
+
+
+
The first step is to import necessary libraries. As using FastHTML inside a Jupyter notebook is a special case, it remains a special import.
+
+
from fasthtml.common import*
+from fasthtml.jupyter import*
+
+
Let’s use jupy_app to instantiate app and rt objects. jupy_app is a thin wrapper around the fast_app function.
Create a server object using JupyUvi, which also starts Uvicorn. The server runs in a separate thread from Jupyter, so it can use normal HTTP client functions in a notebook.
+
+
server = JupyUvi(app)
+
+
The HTMX callable displays the server’s HTMX application in an iframe which can be displayed by Jupyter notebook. Pass in the same port variable used in the JupyUvi callable above.
+
+
# This doesn't display in the docs - uncomment and run it to see it in action
+# HTMX()
+
+
We didn’t define the /click route, but that’s fine - we can define (or change) it any time, and it’s dynamically inserted into the running app. No need to restart or reload anything!
+
+
@rt
+def click(): return P('You clicked me!')
+
+
When you want to gracefully shut down the server use the server.stop() function displayed below. If you restart Jupyter without calling this line the thread may not be released and the HTMX callable above may throw errors. If that happens, a quick temporary fix is to change the port number above to something else.
+
Cleaner solutions to the dangling thread are to kill the dangling thread (dependant on each operating system) or restart the computer.
+
+
server.stop()
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tutorials/jupyter_and_fasthtml.html.md b/tutorials/jupyter_and_fasthtml.html.md
new file mode 100644
index 00000000..d3cb1aa6
--- /dev/null
+++ b/tutorials/jupyter_and_fasthtml.html.md
@@ -0,0 +1,97 @@
+# Using Jupyter to write FastHTML
+
+
+
+
+Writing FastHTML applications in Jupyter notebooks requires a slightly
+different process than normal Python applications.
+
+
+
+> **Download this notebook and try it yourself**
+>
+> The source code for this page is a [Jupyter
+> notebook](https://github.com/AnswerDotAI/fasthtml/blob/main/nbs/tutorials/jupyter_and_fasthtml.ipynb).
+> That makes it easy to directly experiment with it. However, as this is
+> working code that means we have to comment out a few things in order
+> for the documentation to build.
+
+
+
+The first step is to import necessary libraries. As using FastHTML
+inside a Jupyter notebook is a special case, it remains a special
+import.
+
+``` python
+from fasthtml.common import *
+from fasthtml.jupyter import *
+```
+
+Let’s use
+[`jupy_app`](https://AnswerDotAI.github.io/fasthtml/api/jupyter.html#jupy_app)
+to instantiate `app` and `rt` objects.
+[`jupy_app`](https://AnswerDotAI.github.io/fasthtml/api/jupyter.html#jupy_app)
+is a thin wrapper around the
+[`fast_app`](https://AnswerDotAI.github.io/fasthtml/api/fastapp.html#fast_app)
+function.
+
+``` python
+app, rt = jupy_app(pico=True)
+```
+
+Define a route to test the application.
+
+``` python
+@rt
+def index():
+ return Titled('Hello, Jupyter',
+ P('Welcome to the FastHTML + Jupyter example'),
+ Button('Click', hx_get='/click', hx_target='#dest'),
+ Div(id='dest')
+ )
+```
+
+Create a `server` object using
+[`JupyUvi`](https://AnswerDotAI.github.io/fasthtml/api/jupyter.html#jupyuvi),
+which also starts Uvicorn. The `server` runs in a separate thread from
+Jupyter, so it can use normal HTTP client functions in a notebook.
+
+``` python
+server = JupyUvi(app)
+```
+
+The
+[`HTMX`](https://AnswerDotAI.github.io/fasthtml/api/jupyter.html#htmx)
+callable displays the server’s HTMX application in an iframe which can
+be displayed by Jupyter notebook. Pass in the same `port` variable used
+in the
+[`JupyUvi`](https://AnswerDotAI.github.io/fasthtml/api/jupyter.html#jupyuvi)
+callable above.
+
+``` python
+# This doesn't display in the docs - uncomment and run it to see it in action
+# HTMX()
+```
+
+We didn’t define the `/click` route, but that’s fine - we can define (or
+change) it any time, and it’s dynamically inserted into the running app.
+No need to restart or reload anything!
+
+``` python
+@rt
+def click(): return P('You clicked me!')
+```
+
+When you want to gracefully shut down the server use the `server.stop()`
+function displayed below. If you restart Jupyter without calling this
+line the thread may not be released and the
+[`HTMX`](https://AnswerDotAI.github.io/fasthtml/api/jupyter.html#htmx)
+callable above may throw errors. If that happens, a quick temporary fix
+is to change the `port` number above to something else.
+
+Cleaner solutions to the dangling thread are to kill the dangling thread
+(dependant on each operating system) or restart the computer.
+
+``` python
+server.stop()
+```
diff --git a/tutorials/quickstart-web-dev/quickstart-fasthtml.png b/tutorials/quickstart-web-dev/quickstart-fasthtml.png
new file mode 100644
index 00000000..4f4e82b5
Binary files /dev/null and b/tutorials/quickstart-web-dev/quickstart-fasthtml.png differ
diff --git a/tutorials/quickstart_for_web_devs.html b/tutorials/quickstart_for_web_devs.html
new file mode 100644
index 00000000..f28edf5f
--- /dev/null
+++ b/tutorials/quickstart_for_web_devs.html
@@ -0,0 +1,1919 @@
+
+
+
+
+
+
+
+
+
+
+Web Devs Quickstart – fasthtml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+We import what we need for rapid development! A carefully-curated set of FastHTML functions and other Python objects is brought into our global namespace for convenience.
+
+
2
+
+We instantiate a FastHTML app with the fast_app() utility function. This provides a number of really useful defaults that we’ll take advantage of later in the tutorial.
+
+
3
+
+We use the rt() decorator to tell FastHTML what to return when a user visits / in their browser.
+
+
4
+
+We connect this route to HTTP GET requests by defining a view function called get().
+
+
5
+
+A tree of Python function calls that return all the HTML required to write a properly formed web page. You’ll soon see the power of this approach.
+
+
6
+
+The serve() utility configures and runs FastHTML using a library called uvicorn.
+
+
+
Run the code:
+
python main.py
+
The terminal will look like this:
+
INFO: Uvicorn running on http://0.0.0.0:5001 (Press CTRL+C to quit)
+INFO: Started reloader process [58058] using WatchFiles
+INFO: Started server process [58060]
+INFO: Waiting for application startup.
+INFO: Application startup complete.
+
Confirm FastHTML is running by opening your web browser to 127.0.0.1:5001. You should see something like the image below:
+
+
+
+
+
+
+
+Note
+
+
+
+
While some linters and developers will complain about the wildcard import, it is by design here and perfectly safe. FastHTML is very deliberate about the objects it exports in fasthtml.common. If it bothers you, you can import the objects you need individually, though it will make the code more verbose and less readable.
+
If you want to learn more about how FastHTML handles imports, we cover that here.
+
+
+
+
+
A Minimal Charting Application
+
The Script function allows you to include JavaScript. You can use Python to generate parts of your JS or JSON like this:
When we can’t figure out a bug in FastHTML, we can run it in DEBUG mode. When an error is thrown, the error screen is displayed in the browser. This error setting should never be used in a deployed app.
+The “/” URL on line 5 is the home of a project. This would be accessed at 127.0.0.1:5001.
+
+
2
+
+“/hello” URL on line 9 will be found by the project if the user visits 127.0.0.1:5001/hello.
+
+
+
+
+
+
+
+
+Tip
+
+
+
+
It looks like get() is being defined twice, but that’s not the case. Each function decorated with rt is totally separate, and is injected into the router. We’re not calling them in the module’s namespace (locals()). Rather, we’re loading them into the routing mechanism using the rt decorator.
+
+
+
You can do more! Read on to learn what we can do to make parts of the URL dynamic.
+
+
+
Variables in URLs
+
You can add variable sections to a URL by marking them with {variable_name}. Your function then receives the {variable_name} as a keyword argument, but only if it is the correct type. Here’s an example:
+We define two function arguments named identically to the variables. You will note that we specify the Python types to be passed.
+
+
3
+
+We use these functions in our project.
+
+
+
Try it out by going to this address: 127.0.0.1:5001/uma/5. You should get a page that says,
+
+
“Hello Uma, age 5”.
+
+
+
What happens if we enter incorrect data?
+
The 127.0.0.1:5001/uma/5 URL works because 5 is an integer. If we enter something that is not, such as 127.0.0.1:5001/uma/five, then FastHTML will return an error instead of a web page.
+
+
+
+
+
+
+FastHTML URL routing supports more complex types
+
+
+
+
The two examples we provide here use Python’s built-in str and int types, but you can use your own types, including more complex ones such as those defined by libraries like attrs, pydantic, and even sqlmodel.
+
+
+
+
+
+
HTTP Methods
+
FastHTML matches function names to HTTP methods. So far the URL routes we’ve defined have been for HTTP GET methods, the most common method for web pages.
+
Form submissions often are sent as HTTP POST. When dealing with more dynamic web page designs, also known as Single Page Apps (SPA for short), the need can arise for other methods such as HTTP PUT and HTTP DELETE. The way FastHTML handles this is by changing the function name.
+By setting pico to False, FastHTML will not include pico.min.css.
+
+
2
+
+This will generate an HTML <link> tag for sourcing the css for Sakura.
+
+
3
+
+If you want an inline styles, the Style() function will put the result into the HTML.
+
+
+
+
+
Other Static Media File Locations
+
As you saw, Script and Link are specific to the most common static media use cases in web apps: including JavaScript, CSS, and images. But it also works with videos and other static media files. The default behavior is to look for these files in the root directory - typically we don’t do anything special to include them. We can change the default directory that is looked in for files by adding the static_path parameter to the fast_app function.
+
app, rt = fast_app(static_path='public')
+
FastHTML also allows us to define a route that uses FileResponse to serve the file at a specified path. This is useful for serving images, videos, and other media files from a different directory without having to change the paths of many files. So if we move the directory containing the media files, we only need to change the path in one place. In the example below, we call images from a directory called public.
from fasthtml.common import*
+
+hdrs = (MarkdownJS(), HighlightJS(langs=['python', 'javascript', 'html', 'css']), )
+
+app, rt = fast_app(hdrs=hdrs)
+
+content ="""
+Here are some _markdown_ elements.
+
+- This is a list item
+- This is another list item
+- And this is a third list item
+
+**Fenced code blocks work here.**
+"""
+
+@rt('/')
+def get(req):
+return Titled("Markdown rendering example", Div(content,cls="marked"))
+
+serve()
+
+
+
Code highlighting
+
Here’s how to highlight code without any markdown configuration.
+
from fasthtml.common import*
+
+# Add the HighlightJS built-in header
+hdrs = (HighlightJS(langs=['python', 'javascript', 'html', 'css']),)
+
+app, rt = fast_app(hdrs=hdrs)
+
+code_example ="""
+import datetime
+import time
+
+for i in range(10):
+ print(f"{datetime.datetime.now()}")
+ time.sleep(1)
+"""
+
+@rt('/')
+def get(req):
+return Titled("Markdown rendering example",
+ Div(
+# The code example needs to be surrounded by
+# Pre & Code elements
+ Pre(Code(code_example))
+ ))
+
+serve()
+
+
+
Defining new ft components
+
We can build our own ft components and combine them with other components. The simplest method is defining them as a function.
+
+
from fasthtml.common import*
+
+
+
def hero(title, statement):
+return Div(H1(title),P(statement), cls="hero")
+
+# usage example
+Main(
+ hero("Hello World", "This is a hero statement")
+)
+
+
<main><div class="hero">
+<h1>Hello World</h1>
+<p>This is a hero statement</p>
+</div>
+</main>
+
+
+
+
Pass through components
+
For when we need to define a new component that allows zero-to-many components to be nested within them, we lean on Python’s *args and **kwargs mechanism. Useful for creating page layout controls.
+
+
def layout(*args, **kwargs):
+"""Dashboard layout for all our dashboard views"""
+return Main(
+ H1("Dashboard"),
+ Div(*args, **kwargs),
+ cls="dashboard",
+ )
+
+# usage example
+layout(
+ Ul(*[Li(o) for o inrange(3)]),
+ P("Some content", cls="description"),
+)
While functions are easy to read, for more complex components some might find it easier to use a dataclass.
+
+
from dataclasses import dataclass
+
+@dataclass
+class Hero:
+ title: str
+ statement: str
+
+def __ft__(self):
+""" The __ft__ method renders the dataclass at runtime."""
+return Div(H1(self.title),P(self.statement), cls="hero")
+
+# usage example
+Main(
+ Hero("Hello World", "This is a hero statement")
+)
+
+
<main><div class="hero">
+<h1>Hello World</h1>
+<p>This is a hero statement</p>
+</div>
+</main>
+
+
+
+
+
+
Testing views in notebooks
+
Because of the ASGI event loop it is currently impossible to run FastHTML inside a notebook. However, we can still test the output of our views. To do this, we leverage Starlette, an ASGI toolkit that FastHTML uses.
+
+
# First we instantiate our app, in this case we remove the
+# default headers to reduce the size of the output.
+app, rt = fast_app(default_hdrs=False)
+
+# Setting up the Starlette test client
+from starlette.testclient import TestClient
+client = TestClient(app)
+
+# Usage example
+@rt("/")
+def get():
+return Titled("FastHTML is awesome",
+ P("The fastest way to create web apps in Python"))
+
+print(client.get("/").text)
+
+
<!doctype html>
+ <html>
+ <head>
+<title>FastHTML is awesome</title> </head>
+ <body>
+<main class="container"> <h1>FastHTML is awesome</h1>
+ <p>The fastest way to create web apps in Python</p>
+</main> </body>
+ </html>
+
+
+
+
+
+
Forms
+
To validate data coming from users, first define a dataclass representing the data you want to check. Here’s an example representing a signup form.
The usefulness of FastHTML forms becomes more apparent when they are combined with FastHTML views. We’ll show how this works by using the test client from above. First, let’s create a SQlite database:
+
+
db = Database("profiles.db")
+profiles = db.create(Profile, pk="email")
The general rules for rendering are: - __ft__ method will be called (for default components like P, H2, etc. or if you define your own components) - If you pass a string, it will be escaped - On other python objects, str() will be called
+
As a consequence, if you want to include plain HTML tags directly into e.g. a Div() they will get escaped by default (as a security measure to avoid code injections). This can be avoided by using NotStr(), a convenient way to reuse python code that returns already HTML. If you use pandas, you can use pandas.DataFrame.to_html() to get a nice table. To include the output a FastHTML, wrap it in NotStr(), like Div(NotStr(df.to_html())).
+
Above we saw how a dataclass behaves with the __ft__ method defined. On a plain dataclass, str() will be called (but not escaped).
+
+
from dataclasses import dataclass
+
+@dataclass
+class Hero:
+ title: str
+ statement: str
+
+# rendering the dataclass with the default method
+Main(
+ Hero("<h1>Hello World</h1>", "This is a hero statement")
+)
+
+
<main>Hero(title='<h1>Hello World</h1>', statement='This is a hero statement')</main>
+
+
+
+
# This will display the HTML as text on your page
+Div("Let's include some HTML here: <div>Some HTML</div>")
+
+
<div>Let's include some HTML here: <div>Some HTML</div></div>
+
+
+
+
# Keep the string untouched, will be rendered on the page
+Div(NotStr("<div><h1>Some HTML</h1></div>"))
+
+
<div><div><h1>Some HTML</h1></div></div>
+
+
+
+
+
Custom exception handlers
+
FastHTML allows customization of exception handlers, but does so gracefully. What this means is by default it includes all the <html> tags needed to display attractive content. Try it out!
We can set cookies using the cookie() function. In our example, we’ll create a timestamp cookie.
+
+
from datetime import datetime
+from IPython.display import HTML
+
+
+
@rt("/settimestamp")
+def get(req):
+ now = datetime.now()
+return P(f'Set to {now}'), cookie('now', datetime.now())
+
+HTML(client.get('/settimestamp').text)
+
+
+
+
+FastHTML page
+
+
Set to 2024-09-26 15:33:48.141869
+
+
+
+
+
Now let’s get it back using the same name for our parameter as the cookie name.
+
+
@rt('/gettimestamp')
+def get(now:parsed_date): returnf'Cookie was set at time {now.time()}'
+
+client.get('/gettimestamp').text
+
+
'Cookie was set at time 15:33:48.141903'
+
+
+
+
+
Sessions
+
For convenience and security, FastHTML has a mechanism for storing small amounts of data in the user’s browser. We can do this by adding a session argument to routes. FastHTML sessions are Python dictionaries, and we can leverage to our benefit. The example below shows how to concisely set and get sessions.
+
+
@rt('/adder/{num}')
+def get(session, num: int):
+ session.setdefault('sum', 0)
+ session['sum'] = session.get('sum') + num
+return Response(f'The sum is {session["sum"]}.')
+
+
+
+
Toasts (also known as Messages)
+
Toasts, sometimes called “Messages” are small notifications usually in colored boxes used to notify users that something has happened. Toasts can be of four types:
+
+
info
+
success
+
warning
+
error
+
+
Examples toasts might include:
+
+
“Payment accepted”
+
“Data submitted”
+
“Request approved”
+
+
Toasts require the use of the setup_toasts() function plus every view needs these two features:
+
+
The session argument
+
Must return FT components
+
+
1setup_toasts(app)
+
+@rt('/toasting')
+2def get(session):
+# Normally one toast is enough, this allows us to see
+# different toast types in action.
+ add_toast(session, f"Toast is being cooked", "info")
+ add_toast(session, f"Toast is ready", "success")
+ add_toast(session, f"Toast is getting a bit crispy", "warning")
+ add_toast(session, f"Toast is burning!", "error")
+3return Titled("I like toast")
+
+
1
+
+setup_toasts is a helper function that adds toast dependencies. Usually this would be declared right after fast_app()
+
+
2
+
+Toasts require sessions
+
+
3
+
+Views with Toasts must return FT components.
+
+
+
+
+
Authentication and authorization
+
In FastHTML the tasks of authentication and authorization are handled with Beforeware. Beforeware are functions that run before the route handler is called. They are useful for global tasks like ensuring users are authenticated or have permissions to access a view.
+
First, we write a function that accepts a request and session arguments:
+
+
# Status code 303 is a redirect that can change POST to GET,
+# so it's appropriate for a login page.
+login_redir = RedirectResponse('/login', status_code=303)
+
+def user_auth_before(req, sess):
+# The `auth` key in the request scope is automatically provided
+# to any handler which requests it, and can not be injected
+# by the user using query params, cookies, etc, so it should
+# be secure to use.
+ auth = req.scope['auth'] = sess.get('auth', None)
+# If the session key is not there, it redirects to the login page.
+ifnot auth: return login_redir
+
+
Now we pass our user_auth_before function as the first argument into a Beforeware class. We also pass a list of regular expressions to the skip argument, designed to allow users to still get to the home and login pages.
With server-sent events, it’s possible for a server to send new data to a web page at any time, by pushing messages to the web page. Unlike WebSockets, SSE can only go in one direction: server to client. SSE is also part of the HTTP specification unlike WebSockets which uses its own specification.
+
FastHTML introduces several tools for working with SSE which are covered in the example below. While concise, there’s a lot going on in this function so we’ve annotated it quite a bit.
+
+
import random
+from asyncio import sleep
+from fasthtml.common import*
+
+1hdrs=(Script(src="https://unpkg.com/htmx-ext-sse@2.2.1/sse.js"),)
+app,rt = fast_app(hdrs=hdrs)
+
+@rt
+def index():
+return Titled("SSE Random Number Generator",
+ P("Generate pairs of random numbers, as the list grows scroll downwards."),
+2 Div(hx_ext="sse",
+3 sse_connect="/number-stream",
+4 hx_swap="beforeend show:bottom",
+5 sse_swap="message"))
+
+6shutdown_event = signal_shutdown()
+
+7asyncdef number_generator():
+8whilenot shutdown_event.is_set():
+ data = Article(random.randint(1, 100))
+9yield sse_message(data)
+await sleep(1)
+
+@rt("/number-stream")
+10asyncdef get(): return EventStream(number_generator())
+
+
+
1
+
+Import the HTMX SSE extension
+
+
2
+
+Tell HTMX to load the SSE extension
+
+
3
+
+Look at the /number-stream endpoint for SSE content
+
+
4
+
+When new items come in from the SSE endpoint, add them at the end of the current content within the div. If they go beyond the screen, scroll downwards
+
+
5
+
+Specify the name of the event. FastHTML’s default event name is “message”. Only change if you have more than one call to SSE endpoints within a view
+
+
6
+
+Set up the asyncio event loop
+
+
7
+
+Don’t forget to make this an async function!
+
+
8
+
+Iterate through the asyncio event loop
+
+
9
+
+We yield the data. Data ideally should be comprised of FT components as that plugs nicely into HTMX in the browser
+
+
10
+
+The endpoint view needs to be an async function that returns a EventStream
+
+
+
+
+
+
+
Websockets
+
With websockets we can have bi-directional communications between a browser and client. Websockets are useful for things like chat and certain types of games. While websockets can be used for single direction messages from the server (i.e. telling users that a process is finished), that task is arguably better suited for SSE.
+
FastHTML provides useful tools for adding websockets to your pages.
+To use websockets in FastHTML, you must instantiate the app with the ws_hdr set to True
+
+
2
+
+As we want to use websockets to reset the form, we define the mk_input function that can be called from multiple locations
+
+
3
+
+We create the form and mark it with the ws_send attribute, which is documented here in the HTMX websocket specification. This tells HTMX to send a message to the nearest websocket based on the trigger for the form element, which for forms is pressing the enter key, an action considered to be a form submission
+
+
4
+
+This is where the HTMX extension is loaded (hx_ext='ws') and the nearest websocket is defined (ws_connect='/ws')
+
+
5
+
+When a websocket first connects we can optionally have it call a function that accepts a send argument. The send argument will push a message to the browser.
+
+
6
+
+Here we use the send function that was passed into the on_connect function to send a Div with an id of notifications that HTMX assigns to the element in the page that already has an id of notifications
+
+
7
+
+When a websocket disconnects we can call a function which takes no arguments. Typically the role of this function is to notify the server to take an action. In this case, we print a simple message to the console
+
+
8
+
+We use the app.ws decorator to mark that /ws is the route for our websocket. We also pass in the two optional conn and disconn parameters to this decorator. As a fun experiment, remove the conn and disconn arguments and see what happens
+
+
9
+
+Define the ws function as async. This is necessary for ASGI to be able to serve websockets. The function accepts two arguments, a msg that is user input from the browser, and a send function for pushing data back to the browser
+
+
10
+
+The send function is used here to send HTML back to the page. As the HTML has an id of notifications, HTMX will overwrite what is already on the page with the same ID
+
+
11
+
+The websocket function can also be used to return a value. In this case, it is a tuple of two HTML elements. HTMX will take the elements and replace them where appropriate. As both have id specified (notifications and msg respectively), they will replace their predecessor on the page.
+
+
+
+
+
+
+
File Uploads
+
A common task in web development is uploading files. This example is for uploading files to the hosting server, with information about the uploaded file presented to the user.
+
+
+
+
+
+
+File uploads in production can be dangerous
+
+
+
+
File uploads can be the target of uabuse, accidental or intentional. That means users may attempt to upload files that are too large or present a security risk. This is especially of concern for public facing apps. File upload security is outside the scope of this tutorial, for now we suggest reading the OWASP File Upload Cheat Sheet.
+Every form rendered with the Form FT component defaults to enctype="multipart/form-data"
+
+
2
+
+Don’t forget to set the Input FT Component’s type to file
+
+
3
+
+The upload view should receive a Starlette UploadFile type. You can add other form variables
+
+
4
+
+We can access the metadata of the card (filename, size, content_type, headers), a quick and safe process
+
+
5
+
+In order to access the contents contained within a file we use the await method to read() it. As files may be quite large or contain bad data, this is a seperate step from accessing metadata
+
+
6
+
+This step shows how to use Python’s built-in pathlib.Path library to write the file to disk.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tutorials/quickstart_for_web_devs.html.md b/tutorials/quickstart_for_web_devs.html.md
new file mode 100644
index 00000000..acc4a8e8
--- /dev/null
+++ b/tutorials/quickstart_for_web_devs.html.md
@@ -0,0 +1,1219 @@
+# Web Devs Quickstart
+
+
+
+
+## Installation
+
+``` bash
+pip install python-fasthtml
+```
+
+## A Minimal Application
+
+A minimal FastHTML application looks something like this:
+
+
+
+Line 1
+We import what we need for rapid development! A carefully-curated set of
+FastHTML functions and other Python objects is brought into our global
+namespace for convenience.
+
+Line 3
+We instantiate a FastHTML app with the `fast_app()` utility function.
+This provides a number of really useful defaults that we’ll take
+advantage of later in the tutorial.
+
+Line 5
+We use the `rt()` decorator to tell FastHTML what to return when a user
+visits `/` in their browser.
+
+Line 6
+We connect this route to HTTP GET requests by defining a view function
+called `get()`.
+
+Line 7
+A tree of Python function calls that return all the HTML required to
+write a properly formed web page. You’ll soon see the power of this
+approach.
+
+Line 9
+The `serve()` utility configures and runs FastHTML using a library
+called `uvicorn`.
+
+Run the code:
+
+``` bash
+python main.py
+```
+
+The terminal will look like this:
+
+``` bash
+INFO: Uvicorn running on http://0.0.0.0:5001 (Press CTRL+C to quit)
+INFO: Started reloader process [58058] using WatchFiles
+INFO: Started server process [58060]
+INFO: Waiting for application startup.
+INFO: Application startup complete.
+```
+
+Confirm FastHTML is running by opening your web browser to
+[127.0.0.1:5001](http://127.0.0.1:5001). You should see something like
+the image below:
+
+![](quickstart-web-dev/quickstart-fasthtml.png)
+
+
+
+> **Note**
+>
+> While some linters and developers will complain about the wildcard
+> import, it is by design here and perfectly safe. FastHTML is very
+> deliberate about the objects it exports in `fasthtml.common`. If it
+> bothers you, you can import the objects you need individually, though
+> it will make the code more verbose and less readable.
+>
+> If you want to learn more about how FastHTML handles imports, we cover
+> that [here](https://docs.fastht.ml/explains/faq.html#why-use-import).
+
+
+
+## A Minimal Charting Application
+
+The
+[`Script`](https://AnswerDotAI.github.io/fasthtml/api/xtend.html#script)
+function allows you to include JavaScript. You can use Python to
+generate parts of your JS or JSON like this:
+
+``` python
+import json
+from fasthtml.common import *
+
+app, rt = fast_app(hdrs=(Script(src="https://cdn.plot.ly/plotly-2.32.0.min.js"),))
+
+data = json.dumps({
+ "data": [{"x": [1, 2, 3, 4],"type": "scatter"},
+ {"x": [1, 2, 3, 4],"y": [16, 5, 11, 9],"type": "scatter"}],
+ "title": "Plotly chart in FastHTML ",
+ "description": "This is a demo dashboard",
+ "type": "scatter"
+})
+
+
+@rt("/")
+def get():
+ return Titled("Chart Demo", Div(id="myDiv"),
+ Script(f"var data = {data}; Plotly.newPlot('myDiv', data);"))
+
+serve()
+```
+
+## Debug Mode
+
+When we can’t figure out a bug in FastHTML, we can run it in `DEBUG`
+mode. When an error is thrown, the error screen is displayed in the
+browser. This error setting should never be used in a deployed app.
+
+``` python
+from fasthtml.common import *
+
+app, rt = fast_app(debug=True)
+
+@rt("/")
+def get():
+ 1/0
+ return Titled("FastHTML Error!", P("Let's error!"))
+
+serve()
+```
+
+Line 3
+`debug=True` sets debug mode on.
+
+Line 7
+Python throws an error when it tries to divide an integer by zero.
+
+## Routing
+
+FastHTML builds upon FastAPI’s friendly decorator pattern for specifying
+URLs, with extra features:
+
+
+
+Line 5
+The “/” URL on line 5 is the home of a project. This would be accessed
+at [127.0.0.1:5001](http://127.0.0.1:5001).
+
+Line 9
+“/hello” URL on line 9 will be found by the project if the user visits
+[127.0.0.1:5001/hello](http://127.0.0.1:5001/hello).
+
+
+
+> **Tip**
+>
+> It looks like `get()` is being defined twice, but that’s not the case.
+> Each function decorated with `rt` is totally separate, and is injected
+> into the router. We’re not calling them in the module’s namespace
+> (`locals()`). Rather, we’re loading them into the routing mechanism
+> using the `rt` decorator.
+
+
+
+You can do more! Read on to learn what we can do to make parts of the
+URL dynamic.
+
+## Variables in URLs
+
+You can add variable sections to a URL by marking them with
+`{variable_name}`. Your function then receives the `{variable_name}` as
+a keyword argument, but only if it is the correct type. Here’s an
+example:
+
+
+
+Line 5
+We specify two variable names, `name` and `age`.
+
+Line 6
+We define two function arguments named identically to the variables. You
+will note that we specify the Python types to be passed.
+
+Line 7
+We use these functions in our project.
+
+Try it out by going to this address:
+[127.0.0.1:5001/uma/5](http://127.0.0.1:5001/uma/5). You should get a
+page that says,
+
+> “Hello Uma, age 5”.
+
+### What happens if we enter incorrect data?
+
+The [127.0.0.1:5001/uma/5](http://127.0.0.1:5001/uma/5) URL works
+because `5` is an integer. If we enter something that is not, such as
+[127.0.0.1:5001/uma/five](http://127.0.0.1:5001/uma/five), then FastHTML
+will return an error instead of a web page.
+
+
+
+> **FastHTML URL routing supports more complex types**
+>
+> The two examples we provide here use Python’s built-in `str` and `int`
+> types, but you can use your own types, including more complex ones
+> such as those defined by libraries like
+> [attrs](https://pypi.org/project/attrs/),
+> [pydantic](https://pypi.org/project/pydantic/), and even
+> [sqlmodel](https://pypi.org/project/sqlmodel/).
+
+
+
+## HTTP Methods
+
+FastHTML matches function names to HTTP methods. So far the URL routes
+we’ve defined have been for HTTP GET methods, the most common method for
+web pages.
+
+Form submissions often are sent as HTTP POST. When dealing with more
+dynamic web page designs, also known as Single Page Apps (SPA for
+short), the need can arise for other methods such as HTTP PUT and HTTP
+DELETE. The way FastHTML handles this is by changing the function name.
+
+
+
+Line 6
+On line 6 because the `get()` function name is used, this will handle
+HTTP GETs going to the `/` URI.
+
+Line 10
+On line 10 because the `post()` function name is used, this will handle
+HTTP POSTs going to the `/` URI.
+
+## CSS Files and Inline Styles
+
+Here we modify default headers to demonstrate how to use the [Sakura CSS
+microframework](https://github.com/oxalorg/sakura) instead of FastHTML’s
+default of Pico CSS.
+
+
+
+Line 4
+By setting `pico` to `False`, FastHTML will not include `pico.min.css`.
+
+Line 7
+This will generate an HTML `` tag for sourcing the css for Sakura.
+
+Line 8
+If you want an inline styles, the `Style()` function will put the result
+into the HTML.
+
+## Other Static Media File Locations
+
+As you saw,
+[`Script`](https://AnswerDotAI.github.io/fasthtml/api/xtend.html#script)
+and `Link` are specific to the most common static media use cases in web
+apps: including JavaScript, CSS, and images. But it also works with
+videos and other static media files. The default behavior is to look for
+these files in the root directory - typically we don’t do anything
+special to include them. We can change the default directory that is
+looked in for files by adding the `static_path` parameter to the
+[`fast_app`](https://AnswerDotAI.github.io/fasthtml/api/fastapp.html#fast_app)
+function.
+
+``` python
+app, rt = fast_app(static_path='public')
+```
+
+FastHTML also allows us to define a route that uses `FileResponse` to
+serve the file at a specified path. This is useful for serving images,
+videos, and other media files from a different directory without having
+to change the paths of many files. So if we move the directory
+containing the media files, we only need to change the path in one
+place. In the example below, we call images from a directory called
+`public`.
+
+``` python
+@rt("/{fname:path}.{ext:static}")
+async def get(fname:str, ext:str):
+ return FileResponse(f'public/{fname}.{ext}')
+```
+
+## Rendering Markdown
+
+``` python
+from fasthtml.common import *
+
+hdrs = (MarkdownJS(), HighlightJS(langs=['python', 'javascript', 'html', 'css']), )
+
+app, rt = fast_app(hdrs=hdrs)
+
+content = """
+Here are some _markdown_ elements.
+
+- This is a list item
+- This is another list item
+- And this is a third list item
+
+**Fenced code blocks work here.**
+"""
+
+@rt('/')
+def get(req):
+ return Titled("Markdown rendering example", Div(content,cls="marked"))
+
+serve()
+```
+
+## Code highlighting
+
+Here’s how to highlight code without any markdown configuration.
+
+``` python
+from fasthtml.common import *
+
+# Add the HighlightJS built-in header
+hdrs = (HighlightJS(langs=['python', 'javascript', 'html', 'css']),)
+
+app, rt = fast_app(hdrs=hdrs)
+
+code_example = """
+import datetime
+import time
+
+for i in range(10):
+ print(f"{datetime.datetime.now()}")
+ time.sleep(1)
+"""
+
+@rt('/')
+def get(req):
+ return Titled("Markdown rendering example",
+ Div(
+ # The code example needs to be surrounded by
+ # Pre & Code elements
+ Pre(Code(code_example))
+ ))
+
+serve()
+```
+
+## Defining new `ft` components
+
+We can build our own `ft` components and combine them with other
+components. The simplest method is defining them as a function.
+
+``` python
+from fasthtml.common import *
+```
+
+``` python
+def hero(title, statement):
+ return Div(H1(title),P(statement), cls="hero")
+
+# usage example
+Main(
+ hero("Hello World", "This is a hero statement")
+)
+```
+
+``` html
+
+
Hello World
+
This is a hero statement
+
+
+```
+
+### Pass through components
+
+For when we need to define a new component that allows zero-to-many
+components to be nested within them, we lean on Python’s `*args` and
+`**kwargs` mechanism. Useful for creating page layout controls.
+
+``` python
+def layout(*args, **kwargs):
+ """Dashboard layout for all our dashboard views"""
+ return Main(
+ H1("Dashboard"),
+ Div(*args, **kwargs),
+ cls="dashboard",
+ )
+
+# usage example
+layout(
+ Ul(*[Li(o) for o in range(3)]),
+ P("Some content", cls="description"),
+)
+```
+
+``` html
+
Dashboard
+
+
+
0
+
1
+
2
+
+
Some content
+
+
+```
+
+### Dataclasses as ft components
+
+While functions are easy to read, for more complex components some might
+find it easier to use a dataclass.
+
+``` python
+from dataclasses import dataclass
+
+@dataclass
+class Hero:
+ title: str
+ statement: str
+
+ def __ft__(self):
+ """ The __ft__ method renders the dataclass at runtime."""
+ return Div(H1(self.title),P(self.statement), cls="hero")
+
+# usage example
+Main(
+ Hero("Hello World", "This is a hero statement")
+)
+```
+
+``` html
+
+
Hello World
+
This is a hero statement
+
+
+```
+
+## Testing views in notebooks
+
+Because of the ASGI event loop it is currently impossible to run
+FastHTML inside a notebook. However, we can still test the output of our
+views. To do this, we leverage Starlette, an ASGI toolkit that FastHTML
+uses.
+
+``` python
+# First we instantiate our app, in this case we remove the
+# default headers to reduce the size of the output.
+app, rt = fast_app(default_hdrs=False)
+
+# Setting up the Starlette test client
+from starlette.testclient import TestClient
+client = TestClient(app)
+
+# Usage example
+@rt("/")
+def get():
+ return Titled("FastHTML is awesome",
+ P("The fastest way to create web apps in Python"))
+
+print(client.get("/").text)
+```
+
+
+
+
+ FastHTML is awesome
+
+
FastHTML is awesome
+
The fastest way to create web apps in Python
+
+
+
+## Forms
+
+To validate data coming from users, first define a dataclass
+representing the data you want to check. Here’s an example representing
+a signup form.
+
+``` python
+from dataclasses import dataclass
+
+@dataclass
+class Profile: email:str; phone:str; age:int
+```
+
+Create an FT component representing an empty version of that form. Don’t
+pass in any value to fill the form, that gets handled later.
+
+``` python
+profile_form = Form(method="post", action="/profile")(
+ Fieldset(
+ Label('Email', Input(name="email")),
+ Label("Phone", Input(name="phone")),
+ Label("Age", Input(name="age")),
+ ),
+ Button("Save", type="submit"),
+ )
+profile_form
+```
+
+``` html
+
+```
+
+Once the dataclass and form function are completed, we can add data to
+the form. To do that, instantiate the profile dataclass:
+
+``` python
+profile = Profile(email='john@example.com', phone='123456789', age=5)
+profile
+```
+
+ Profile(email='john@example.com', phone='123456789', age=5)
+
+Then add that data to the `profile_form` using FastHTML’s
+[`fill_form`](https://AnswerDotAI.github.io/fasthtml/api/components.html#fill_form)
+class:
+
+``` python
+fill_form(profile_form, profile)
+```
+
+``` html
+
+```
+
+### Forms with views
+
+The usefulness of FastHTML forms becomes more apparent when they are
+combined with FastHTML views. We’ll show how this works by using the
+test client from above. First, let’s create a SQlite database:
+
+``` python
+db = Database("profiles.db")
+profiles = db.create(Profile, pk="email")
+```
+
+Now we insert a record into the database:
+
+``` python
+profiles.insert(profile)
+```
+
+ Profile(email='john@example.com', phone='123456789', age=5)
+
+And we can then demonstrate in the code that form is filled and
+displayed to the user.
+
+``` python
+@rt("/profile/{email}")
+def profile(email:str):
+ profile = profiles[email]
+ filled_profile_form = fill_form(profile_form, profile)
+ return Titled(f'Profile for {profile.email}', filled_profile_form)
+
+print(client.get(f"/profile/john@example.com").text)
+```
+
+Line 3
+Fetch the profile using the profile table’s `email` primary key
+
+Line 4
+Fill the form for display.
+
+
+
+
+
+
+ Profile for john@example.com
+
+
Profile for john@example.com
+
+
+
+And now let’s demonstrate making a change to the data.
+
+``` python
+@rt("/profile")
+def post(profile: Profile):
+ profiles.update(profile)
+ return RedirectResponse(url=f"/profile/{profile.email}")
+
+new_data = dict(email='john@example.com', phone='7654321', age=25)
+print(client.post("/profile", data=new_data).text)
+```
+
+Line 2
+We use the `Profile` dataclass definition to set the type for the
+incoming `profile` content. This validates the field types for the
+incoming data
+
+Line 3
+Taking our validated data, we updated the profiles table
+
+Line 4
+We redirect the user back to their profile view
+
+Line 7
+The display is of the profile form view showing the changes in data.
+
+
+
+
+
+
+ Profile for john@example.com
+
+
Profile for john@example.com
+
+
+
+## Strings and conversion order
+
+The general rules for rendering are: - `__ft__` method will be called
+(for default components like `P`, `H2`, etc. or if you define your own
+components) - If you pass a string, it will be escaped - On other python
+objects, `str()` will be called
+
+As a consequence, if you want to include plain HTML tags directly into
+e.g. a `Div()` they will get escaped by default (as a security measure
+to avoid code injections). This can be avoided by using `NotStr()`, a
+convenient way to reuse python code that returns already HTML. If you
+use pandas, you can use `pandas.DataFrame.to_html()` to get a nice
+table. To include the output a FastHTML, wrap it in `NotStr()`, like
+`Div(NotStr(df.to_html()))`.
+
+Above we saw how a dataclass behaves with the `__ft__` method defined.
+On a plain dataclass, `str()` will be called (but not escaped).
+
+``` python
+from dataclasses import dataclass
+
+@dataclass
+class Hero:
+ title: str
+ statement: str
+
+# rendering the dataclass with the default method
+Main(
+ Hero("
Hello World
", "This is a hero statement")
+)
+```
+
+``` html
+Hero(title='
Hello World
', statement='This is a hero statement')
+```
+
+``` python
+# This will display the HTML as text on your page
+Div("Let's include some HTML here:
Some HTML
")
+```
+
+``` html
+
Let's include some HTML here: <div>Some HTML</div>
+```
+
+``` python
+# Keep the string untouched, will be rendered on the page
+Div(NotStr("
Some HTML
"))
+```
+
+``` html
+
Some HTML
+```
+
+## Custom exception handlers
+
+FastHTML allows customization of exception handlers, but does so
+gracefully. What this means is by default it includes all the ``
+tags needed to display attractive content. Try it out!
+
+``` python
+from fasthtml.common import *
+
+def not_found(req, exc): return Titled("404: I don't exist!")
+
+exception_handlers = {404: not_found}
+
+app, rt = fast_app(exception_handlers=exception_handlers)
+
+@rt('/')
+def get():
+ return (Titled("Home page", P(A(href="/oops")("Click to generate 404 error"))))
+
+serve()
+```
+
+We can also use lambda to make things more terse:
+
+``` python
+from fasthtml.common import *
+
+exception_handlers={
+ 404: lambda req, exc: Titled("404: I don't exist!"),
+ 418: lambda req, exc: Titled("418: I'm a teapot!")
+}
+
+app, rt = fast_app(exception_handlers=exception_handlers)
+
+@rt('/')
+def get():
+ return (Titled("Home page", P(A(href="/oops")("Click to generate 404 error"))))
+
+serve()
+```
+
+## Cookies
+
+We can set cookies using the `cookie()` function. In our example, we’ll
+create a `timestamp` cookie.
+
+``` python
+from datetime import datetime
+from IPython.display import HTML
+```
+
+``` python
+@rt("/settimestamp")
+def get(req):
+ now = datetime.now()
+ return P(f'Set to {now}'), cookie('now', datetime.now())
+
+HTML(client.get('/settimestamp').text)
+```
+
+
+
+
+FastHTML page
+
+
Set to 2024-09-26 15:33:48.141869
+
+
+
+Now let’s get it back using the same name for our parameter as the
+cookie name.
+
+``` python
+@rt('/gettimestamp')
+def get(now:parsed_date): return f'Cookie was set at time {now.time()}'
+
+client.get('/gettimestamp').text
+```
+
+ 'Cookie was set at time 15:33:48.141903'
+
+## Sessions
+
+For convenience and security, FastHTML has a mechanism for storing small
+amounts of data in the user’s browser. We can do this by adding a
+`session` argument to routes. FastHTML sessions are Python dictionaries,
+and we can leverage to our benefit. The example below shows how to
+concisely set and get sessions.
+
+``` python
+@rt('/adder/{num}')
+def get(session, num: int):
+ session.setdefault('sum', 0)
+ session['sum'] = session.get('sum') + num
+ return Response(f'The sum is {session["sum"]}.')
+```
+
+## Toasts (also known as Messages)
+
+Toasts, sometimes called “Messages” are small notifications usually in
+colored boxes used to notify users that something has happened. Toasts
+can be of four types:
+
+- info
+- success
+- warning
+- error
+
+Examples toasts might include:
+
+- “Payment accepted”
+- “Data submitted”
+- “Request approved”
+
+Toasts require the use of the `setup_toasts()` function plus every view
+needs these two features:
+
+- The session argument
+- Must return FT components
+
+``` python
+setup_toasts(app)
+
+@rt('/toasting')
+def get(session):
+ # Normally one toast is enough, this allows us to see
+ # different toast types in action.
+ add_toast(session, f"Toast is being cooked", "info")
+ add_toast(session, f"Toast is ready", "success")
+ add_toast(session, f"Toast is getting a bit crispy", "warning")
+ add_toast(session, f"Toast is burning!", "error")
+ return Titled("I like toast")
+```
+
+Line 1
+`setup_toasts` is a helper function that adds toast dependencies.
+Usually this would be declared right after `fast_app()`
+
+Line 4
+Toasts require sessions
+
+Line 11
+Views with Toasts must return FT components.
+
+## Authentication and authorization
+
+In FastHTML the tasks of authentication and authorization are handled
+with Beforeware. Beforeware are functions that run before the route
+handler is called. They are useful for global tasks like ensuring users
+are authenticated or have permissions to access a view.
+
+First, we write a function that accepts a request and session arguments:
+
+``` python
+# Status code 303 is a redirect that can change POST to GET,
+# so it's appropriate for a login page.
+login_redir = RedirectResponse('/login', status_code=303)
+
+def user_auth_before(req, sess):
+ # The `auth` key in the request scope is automatically provided
+ # to any handler which requests it, and can not be injected
+ # by the user using query params, cookies, etc, so it should
+ # be secure to use.
+ auth = req.scope['auth'] = sess.get('auth', None)
+ # If the session key is not there, it redirects to the login page.
+ if not auth: return login_redir
+```
+
+Now we pass our `user_auth_before` function as the first argument into a
+[`Beforeware`](https://AnswerDotAI.github.io/fasthtml/api/core.html#beforeware)
+class. We also pass a list of regular expressions to the `skip`
+argument, designed to allow users to still get to the home and login
+pages.
+
+``` python
+beforeware = Beforeware(
+ user_auth_before,
+ skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css', r'.*\.js', '/login', '/']
+)
+
+app, rt = fast_app(before=beforeware)
+```
+
+## Server-sent events (SSE)
+
+With [server-sent
+events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events),
+it’s possible for a server to send new data to a web page at any time,
+by pushing messages to the web page. Unlike WebSockets, SSE can only go
+in one direction: server to client. SSE is also part of the HTTP
+specification unlike WebSockets which uses its own specification.
+
+FastHTML introduces several tools for working with SSE which are covered
+in the example below. While concise, there’s a lot going on in this
+function so we’ve annotated it quite a bit.
+
+``` python
+import random
+from asyncio import sleep
+from fasthtml.common import *
+
+hdrs=(Script(src="https://unpkg.com/htmx-ext-sse@2.2.1/sse.js"),)
+app,rt = fast_app(hdrs=hdrs)
+
+@rt
+def index():
+ return Titled("SSE Random Number Generator",
+ P("Generate pairs of random numbers, as the list grows scroll downwards."),
+ Div(hx_ext="sse",
+ sse_connect="/number-stream",
+ hx_swap="beforeend show:bottom",
+ sse_swap="message"))
+
+shutdown_event = signal_shutdown()
+
+async def number_generator():
+ while not shutdown_event.is_set():
+ data = Article(random.randint(1, 100))
+ yield sse_message(data)
+ await sleep(1)
+
+@rt("/number-stream")
+async def get(): return EventStream(number_generator())
+```
+
+Line 5
+Import the HTMX SSE extension
+
+Line 12
+Tell HTMX to load the SSE extension
+
+Line 13
+Look at the `/number-stream` endpoint for SSE content
+
+Line 14
+When new items come in from the SSE endpoint, add them at the end of the
+current content within the div. If they go beyond the screen, scroll
+downwards
+
+Line 15
+Specify the name of the event. FastHTML’s default event name is
+“message”. Only change if you have more than one call to SSE endpoints
+within a view
+
+Line 17
+Set up the asyncio event loop
+
+Line 19
+Don’t forget to make this an `async` function!
+
+Line 20
+Iterate through the asyncio event loop
+
+Line 22
+We yield the data. Data ideally should be comprised of FT components as
+that plugs nicely into HTMX in the browser
+
+Line 26
+The endpoint view needs to be an async function that returns a
+[`EventStream`](https://AnswerDotAI.github.io/fasthtml/api/core.html#eventstream)
+
+## Websockets
+
+With websockets we can have bi-directional communications between a
+browser and client. Websockets are useful for things like chat and
+certain types of games. While websockets can be used for single
+direction messages from the server (i.e. telling users that a process is
+finished), that task is arguably better suited for SSE.
+
+FastHTML provides useful tools for adding websockets to your pages.
+
+``` python
+from fasthtml.common import *
+from asyncio import sleep
+
+app, rt = fast_app(ws_hdr=True)
+
+def mk_inp(): return Input(id='msg', autofocus=True)
+
+@rt('/')
+async def get(request):
+ cts = Div(
+ Div(id='notifications'),
+ Form(mk_inp(), id='form', ws_send=True),
+ hx_ext='ws', ws_connect='/ws')
+ return Titled('Websocket Test', cts)
+
+async def on_connect(send):
+ print('Connected!')
+ await send(Div('Hello, you have connected', id="notifications"))
+
+async def on_disconnect(ws):
+ print('Disconnected!')
+
+@app.ws('/ws', conn=on_connect, disconn=on_disconnect)
+async def ws(msg:str, send):
+ await send(Div('Hello ' + msg, id="notifications"))
+ await sleep(2)
+ return Div('Goodbye ' + msg, id="notifications"), mk_inp()
+```
+
+Line 4
+To use websockets in FastHTML, you must instantiate the app with the
+`ws_hdr` set to `True`
+
+Line 6
+As we want to use websockets to reset the form, we define the `mk_input`
+function that can be called from multiple locations
+
+Line 12
+We create the form and mark it with the `ws_send` attribute, which is
+documented here in the [HTMX websocket
+specification](https://v1.htmx.org/extensions/web-sockets/). This tells
+HTMX to send a message to the nearest websocket based on the trigger for
+the form element, which for forms is pressing the `enter` key, an action
+considered to be a form submission
+
+Line 13
+This is where the HTMX extension is loaded (`hx_ext='ws'`) and the
+nearest websocket is defined (`ws_connect='/ws'`)
+
+Line 16
+When a websocket first connects we can optionally have it call a
+function that accepts a `send` argument. The `send` argument will push a
+message to the browser.
+
+Line 18
+Here we use the `send` function that was passed into the `on_connect`
+function to send a `Div` with an `id` of `notifications` that HTMX
+assigns to the element in the page that already has an `id` of
+`notifications`
+
+Line 20
+When a websocket disconnects we can call a function which takes no
+arguments. Typically the role of this function is to notify the server
+to take an action. In this case, we print a simple message to the
+console
+
+Line 23
+We use the `app.ws` decorator to mark that `/ws` is the route for our
+websocket. We also pass in the two optional `conn` and `disconn`
+parameters to this decorator. As a fun experiment, remove the `conn` and
+`disconn` arguments and see what happens
+
+Line 24
+Define the `ws` function as async. This is necessary for ASGI to be able
+to serve websockets. The function accepts two arguments, a `msg` that is
+user input from the browser, and a `send` function for pushing data back
+to the browser
+
+Line 25
+The `send` function is used here to send HTML back to the page. As the
+HTML has an `id` of `notifications`, HTMX will overwrite what is already
+on the page with the same ID
+
+Line 27
+The websocket function can also be used to return a value. In this case,
+it is a tuple of two HTML elements. HTMX will take the elements and
+replace them where appropriate. As both have `id` specified
+(`notifications` and `msg` respectively), they will replace their
+predecessor on the page.
+
+## File Uploads
+
+A common task in web development is uploading files. This example is for
+uploading files to the hosting server, with information about the
+uploaded file presented to the user.
+
+
+
+> **File uploads in production can be dangerous**
+>
+> File uploads can be the target of uabuse, accidental or intentional.
+> That means users may attempt to upload files that are too large or
+> present a security risk. This is especially of concern for public
+> facing apps. File upload security is outside the scope of this
+> tutorial, for now we suggest reading the [OWASP File Upload Cheat
+> Sheet](https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html).
+
+
+
+``` python
+from fasthtml.common import *
+from pathlib import Path
+
+app, rt = fast_app()
+
+upload_dir = Path("filez")
+upload_dir.mkdir(exist_ok=True)
+
+@rt('/')
+def get():
+ return Titled("File Upload Demo",
+ Div(cls='grid')(
+ Article(
+ Form(hx_post="/upload", hx_target="#result-one")(
+ Input(type="file", name="file"),
+ Button("Upload", type="submit", cls='secondary'),
+ ),
+ Div(id="result-one")
+ )
+ )
+ )
+
+def FileMetaDataCard(file):
+ return Article(
+ Header(H3(file.filename)),
+ Ul(
+ Li('Size: ', file.size),
+ Li('Content Type: ', file.content_type),
+ Li('Headers: ', file.headers),
+ )
+ )
+
+@rt('/upload')
+async def post(file: UploadFile):
+ card = FileMetaDataCard(file)
+ filebuffer = await file.read()
+ (upload_dir / file.filename).write_bytes(filebuffer)
+ return card
+
+serve()
+```
+
+Line 14
+Every form rendered with the
+[`Form`](https://AnswerDotAI.github.io/fasthtml/api/xtend.html#form) FT
+component defaults to `enctype="multipart/form-data"`
+
+Line 15
+Don’t forget to set the `Input` FT Component’s type to `file`
+
+Line 34
+The upload view should receive a [Starlette
+UploadFile](https://www.starlette.io/requests/#request-files) type. You
+can add other form variables
+
+Line 35
+We can access the metadata of the card (filename, size, content_type,
+headers), a quick and safe process
+
+Line 36
+In order to access the contents contained within a file we use the
+`await` method to read() it. As files may be quite large or contain bad
+data, this is a seperate step from accessing metadata
+
+Line 37
+This step shows how to use Python’s built-in `pathlib.Path` library to
+write the file to disk.
diff --git a/unpublished/tutorial_for_web_devs.html b/unpublished/tutorial_for_web_devs.html
new file mode 100644
index 00000000..0ff12cc9
--- /dev/null
+++ b/unpublished/tutorial_for_web_devs.html
@@ -0,0 +1,1085 @@
+
+
+
+
+
+
+
+
+
+
+BYO Blog – fasthtml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Learn the foundations of FastHTML by creating your own blogging system from scratch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Caution
+
+
+
+
This document is a work in progress.
+
+
+
In this tutorial we’re going to write a blog by example. Blogs are a good way to learn a web framework as they start simple yet can get surprisingly sophistated. The wikipedia definition of a blog is “an informational website consisting of discrete, often informal diary-style text entries (posts) informal diary-style text entries (posts)”, which means we need to provide these basic features:
+
+
A list of articles
+
A means to create/edit/delete the articles
+
An attractive but accessible layout
+
+
We’ll also add in these features, so the blog can become a working site:
+
+
RSS feed
+
Pages independent of the list of articles (about and contact come to mind)
+
Import and Export of articles
+
Tagging and categorization of data
+
Deployment
+
Ability to scale for large volumes of readers
+
+
+
How to best use this tutorial
+
We could copy/paste every code example in sequence and have a finished blog at the end. However, it’s debatable how much we will learn through the copy/paste method. We’re not saying its impossible to learn through copy/paste, we’re just saying it’s not that of an efficient way to learn. It’s analogous to learning how to play a musical instrument or sport or video game by watching other people do it - you can learn some but its not the same as doing.
+
A better approach is to type out every line of code in this tutorial. This forces us to run the code through our brains, giving us actual practice in how to write FastHTML and Pythoncode and forcing us to debug our own mistakes. In some cases we’ll repeat similar tasks - a key component in achieving mastery in anything. Coming back to the instrument/sport/video game analogy, it’s exactly like actually practicing an instrument, sport, or video game. Through practice and repetition we eventually achieve mastery.
+
+
+
Installing FastHTML
+
FastHTML is just Python. Installation is often done with pip:
+
pip install python-fasthtml
+
+
+
A minimal FastHTML app
+
First, create the directory for our project using Python’s pathlib module:
Now that we have our directory, let’s create a minimal FastHTML site in it.
+
+
+
blog-system/minimal.py
+
+
from fasthtml.common import*
+
+app, rt = fast_app()
+
+@rt("/")
+def get():
+return Titled("FastHTML", P("Let's do this!"))
+
+serve()
+
+
Run that with python minimal.py and you should get something like this:
+
python minimal.py
+Link: http://localhost:5001
+INFO: Will watch for changes in these directories: ['/Users/pydanny/projects/blog-system']
+INFO: Uvicorn running on http://0.0.0.0:5001 (Press CTRL+C to quit)
+INFO: Started reloader process [46572] using WatchFiles
+INFO: Started server process [46576]
+INFO: Waiting for application startup.
+INFO: Application startup complete.
+
Confirm FastHTML is running by opening your web browser to 127.0.0.1:5001. You should see something like the image below:
+
+
+
+
+
+
+
+What about the import *?
+
+
+
+
For those worried about the use of import * rather than a PEP8-style declared namespace, understand that __all__ is defined in FastHTML’s common module. That means that only the symbols (functions, classes, and other things) the framework wants us to have will be brought into our own code via import *. Read importing from a package) for more information.
+
Nevertheless, if we want to use a defined namespace we can do so. Here’s an example:
+
from fasthtml import common as fh
+
+
+app, rt = fh.fast_app()
+
+@rt("/")
+def get():
+return fh.Titled("FastHTML", fh.P("Let's do this!"))
+
+fh.serve()
+
+
+
+
+
Looking more closely at our app
+
Let’s look more closely at our application. Every line is packed with powerful features of FastHTML:
+The top level namespace of Fast HTML (fasthtml.common) contains everything we need from FastHTML to build applications. A carefully-curated set of FastHTML functions and other Python objects is brought into our global namespace for convenience.
+
+
2
+
+We instantiate a FastHTML app with the fast_app() utility function. This provides a number of really useful defaults that we’ll modify or take advantage of later in the tutorial.
+
+
3
+
+We use the rt() decorator to tell FastHTML what to return when a user visits / in their browser.
+
+
4
+
+We connect this route to HTTP GET requests by defining a view function called get().
+
+
5
+
+A tree of Python function calls that return all the HTML required to write a properly formed web page. You’ll soon see the power of this approach.
+
+
6
+
+The serve() utility configures and runs FastHTML using a library called uvicorn. Any changes to this module will be reloaded into the browser.
+
+
+
+
+
Adding dynamic content to our minimal app
+
Our page is great, but we’ll make it better. Let’s add a randomized list of letters to the page. Every time the page reloads, a new list of varying length will be generated.
+
+
+
blog-system/random_letters.py
+
+
from fasthtml.common import*
+1import string, random
+
+app, rt = fast_app()
+
+@rt("/")
+def get():
+2 letters = random.choices(string.ascii_uppercase, k=random.randint(5, 20))
+3 items = [Li(c) for c in letters]
+return Titled("Random lists of letters",
+4 Ul(*items)
+ )
+
+serve()
+
+
+
1
+
+The string and random libraries are part of Python’s standard library
+
+
2
+
+We use these libraries to generate a random length list of random letters called letters
+
+
3
+
+Using letters as the base we use list comprehension to generate a list of Li ft display components, each with their own letter and save that to the variable items
+
+
4
+
+Inside a call to the Ul() ft component we use Python’s *args special syntax on the items variable. Therefore *list is treated not as one argument but rather a set of them.
+
+
+
When this is run, it will generate something like this with a different random list of letters for each page load:
+
+
+
+
Storing the articles
+
The most basic component of a blog is a series of articles sorted by date authored. Rather than a database we’re going to use our computer’s harddrive to store a set of markdown files in a directory within our blog called posts. First, let’s create the directory and some test files we can use to search for:
+
+
from fastcore.utils import*
+
+
+
# Create some dummy posts
+posts = Path("posts")
+posts.mkdir(exist_ok=True)
+for i inrange(10): (posts/f"article_{i}.md").write_text(f"This is article {i}")
+
+
Searching for these files can be done with pathlib.
Python’s pathlib library is quite useful and makes file search and manipulation much easier. There’s many uses for it and is compatible across operating systems.
+
+
+
+
+
Creating the blog home page
+
We now have enough tools that we can create the home page. Let’s create a new Python file and write out our simple view to list the articles in our blog.
+
+
+
+
+
\ No newline at end of file
diff --git a/unpublished/tutorial_for_web_devs.html.md b/unpublished/tutorial_for_web_devs.html.md
new file mode 100644
index 00000000..c36bf8ae
--- /dev/null
+++ b/unpublished/tutorial_for_web_devs.html.md
@@ -0,0 +1,318 @@
+# BYO Blog
+
+
+
+
+
+
+> **Caution**
+>
+> This document is a work in progress.
+
+
+
+In this tutorial we’re going to write a blog by example. Blogs are a
+good way to learn a web framework as they start simple yet can get
+surprisingly sophistated. The [wikipedia definition of a
+blog](https://en.wikipedia.org/wiki/Blog) is “an informational website
+consisting of discrete, often informal diary-style text entries (posts)
+informal diary-style text entries (posts)”, which means we need to
+provide these basic features:
+
+- A list of articles
+- A means to create/edit/delete the articles
+- An attractive but accessible layout
+
+We’ll also add in these features, so the blog can become a working site:
+
+- RSS feed
+- Pages independent of the list of articles (about and contact come to
+ mind)
+- Import and Export of articles
+- Tagging and categorization of data
+- Deployment
+- Ability to scale for large volumes of readers
+
+## How to best use this tutorial
+
+We could copy/paste every code example in sequence and have a finished
+blog at the end. However, it’s debatable how much we will learn through
+the copy/paste method. We’re not saying its impossible to learn through
+copy/paste, we’re just saying it’s not that of an efficient way to
+learn. It’s analogous to learning how to play a musical instrument or
+sport or video game by watching other people do it - you can learn some
+but its not the same as doing.
+
+A better approach is to type out every line of code in this tutorial.
+This forces us to run the code through our brains, giving us actual
+practice in how to write FastHTML and Pythoncode and forcing us to debug
+our own mistakes. In some cases we’ll repeat similar tasks - a key
+component in achieving mastery in anything. Coming back to the
+instrument/sport/video game analogy, it’s exactly like actually
+practicing an instrument, sport, or video game. Through practice and
+repetition we eventually achieve mastery.
+
+## Installing FastHTML
+
+FastHTML is *just Python*. Installation is often done with pip:
+
+``` shellscript
+pip install python-fasthtml
+```
+
+## A minimal FastHTML app
+
+First, create the directory for our project using Python’s
+[pathlib](https://docs.python.org/3/library/pathlib.html) module:
+
+``` python
+import pathlib
+pathlib.Path('blog-system').mkdir()
+```
+
+Now that we have our directory, let’s create a minimal FastHTML site in
+it.
+
+
+
+Run that with `python minimal.py` and you should get something like
+this:
+
+``` shellscript
+python minimal.py
+Link: http://localhost:5001
+INFO: Will watch for changes in these directories: ['/Users/pydanny/projects/blog-system']
+INFO: Uvicorn running on http://0.0.0.0:5001 (Press CTRL+C to quit)
+INFO: Started reloader process [46572] using WatchFiles
+INFO: Started server process [46576]
+INFO: Waiting for application startup.
+INFO: Application startup complete.
+```
+
+Confirm FastHTML is running by opening your web browser to
+[127.0.0.1:5001](http://127.0.0.1:5001). You should see something like
+the image below:
+
+![](quickstart-web-dev/quickstart-fasthtml.png)
+
+
+
+> **What about the `import *`?**
+>
+> For those worried about the use of `import *` rather than a PEP8-style
+> declared namespace, understand that `__all__` is defined in FastHTML’s
+> common module. That means that only the symbols (functions, classes,
+> and other things) the framework wants us to have will be brought into
+> our own code via `import *`. Read [importing from a
+> package](https://docs.python.org/3/tutorial/modules.html#importing-from-a-package))
+> for more information.
+>
+> Nevertheless, if we want to use a defined namespace we can do so.
+> Here’s an example:
+>
+> ``` python
+> from fasthtml import common as fh
+>
+>
+> app, rt = fh.fast_app()
+>
+> @rt("/")
+> def get():
+> return fh.Titled("FastHTML", fh.P("Let's do this!"))
+>
+> fh.serve()
+> ```
+
+
+
+## Looking more closely at our app
+
+Let’s look more closely at our application. Every line is packed with
+powerful features of FastHTML:
+
+
+
+Line 1
+The top level namespace of Fast HTML (fasthtml.common) contains
+everything we need from FastHTML to build applications. A
+carefully-curated set of FastHTML functions and other Python objects is
+brought into our global namespace for convenience.
+
+Line 3
+We instantiate a FastHTML app with the `fast_app()` utility function.
+This provides a number of really useful defaults that we’ll modify or
+take advantage of later in the tutorial.
+
+Line 5
+We use the `rt()` decorator to tell FastHTML what to return when a user
+visits `/` in their browser.
+
+Line 6
+We connect this route to HTTP GET requests by defining a view function
+called `get()`.
+
+Line 7
+A tree of Python function calls that return all the HTML required to
+write a properly formed web page. You’ll soon see the power of this
+approach.
+
+Line 9
+The `serve()` utility configures and runs FastHTML using a library
+called `uvicorn`. Any changes to this module will be reloaded into the
+browser.
+
+## Adding dynamic content to our minimal app
+
+Our page is great, but we’ll make it better. Let’s add a randomized list
+of letters to the page. Every time the page reloads, a new list of
+varying length will be generated.
+
+
+
+**blog-system/random_letters.py**
+
+``` python
+from fasthtml.common import *
+import string, random
+
+app, rt = fast_app()
+
+@rt("/")
+def get():
+ letters = random.choices(string.ascii_uppercase, k=random.randint(5, 20))
+ items = [Li(c) for c in letters]
+ return Titled("Random lists of letters",
+ Ul(*items)
+ )
+
+serve()
+```
+
+
+
+Line 2
+The `string` and `random` libraries are part of Python’s standard
+library
+
+Line 8
+We use these libraries to generate a random length list of random
+letters called `letters`
+
+Line 9
+Using `letters` as the base we use list comprehension to generate a list
+of `Li` ft display components, each with their own letter and save that
+to the variable `items`
+
+Line 11
+Inside a call to the `Ul()` ft component we use Python’s `*args` special
+syntax on the `items` variable. Therefore `*list` is treated not as one
+argument but rather a set of them.
+
+When this is run, it will generate something like this with a different
+random list of letters for each page load:
+
+![](web-dev-tut/random-list-letters.png)
+
+## Storing the articles
+
+The most basic component of a blog is a series of articles sorted by
+date authored. Rather than a database we’re going to use our computer’s
+harddrive to store a set of markdown files in a directory within our
+blog called `posts`. First, let’s create the directory and some test
+files we can use to search for:
+
+``` python
+from fastcore.utils import *
+```
+
+``` python
+# Create some dummy posts
+posts = Path("posts")
+posts.mkdir(exist_ok=True)
+for i in range(10): (posts/f"article_{i}.md").write_text(f"This is article {i}")
+```
+
+Searching for these files can be done with pathlib.
+
+``` python
+import pathlib
+posts.ls()
+```
+
+ (#10) [Path('posts/article_5.md'),Path('posts/article_1.md'),Path('posts/article_0.md'),Path('posts/article_4.md'),Path('posts/article_3.md'),Path('posts/article_7.md'),Path('posts/article_6.md'),Path('posts/article_2.md'),Path('posts/article_9.md'),Path('posts/article_8.md')]
+
+
+
+> **Tip**
+>
+> Python’s [pathlib](https://docs.python.org/3/library/pathlib.html)
+> library is quite useful and makes file search and manipulation much
+> easier. There’s many uses for it and is compatible across operating
+> systems.
+
+
+
+## Creating the blog home page
+
+We now have enough tools that we can create the home page. Let’s create
+a new Python file and write out our simple view to list the articles in
+our blog.
+
+
+
+``` python
+for p in posts.ls(): p.unlink()
+```
diff --git a/unpublished/web-dev-tut/random-list-letters.png b/unpublished/web-dev-tut/random-list-letters.png
new file mode 100644
index 00000000..a63a7d59
Binary files /dev/null and b/unpublished/web-dev-tut/random-list-letters.png differ
Socials
+ +OG and Twitter social card headers
++
source
+