Skip to content
Snippets Groups Projects
Commit 1d468a20 authored by Julian's avatar Julian
Browse files

First prototype

parents
Branches
No related tags found
No related merge requests found
Showing
with 741 additions and 0 deletions
import secrets
import time
import os
from flask import Flask, render_template, request, redirect, url_for, Response, abort, session, flash
from requests_oauthlib import OAuth2Session
from .models import db, Item, Location, Photo
from .utils import render_pdf, print_pdf, qrcode_svg
app = Flask(__name__, instance_relative_config=True)
app.config.from_pyfile('config.cfg')
photo_upload_dir = os.path.join(app.config['UPLOAD_FOLDER'], 'photos')
if not os.path.isdir(photo_upload_dir):
os.mkdir(photo_upload_dir)
app.add_template_filter(qrcode_svg)
db.init_app(app)
@app.before_request
def ensure_auth():
if request.endpoint not in ('static', 'qrauth', 'oauth2_start', 'oauth2_callback', 'logout',):
valid_qrauth = (
session.get('auth') == 'qrauth'
and app.config.get('QRAUTH_CODE')
and session.get('auth_code') == app.config['QRAUTH_CODE']
)
valid_oauth2 = (session.get('auth') == 'oauth2')
valid_timestamp = (time.time() - session.get('auth_timestamp', 0) < 60*60*24)
if (not valid_qrauth and not valid_oauth2) or not valid_timestamp:
return render_template('401.html'), 401
@app.before_request
def csrf_check():
request.csrf_token = session.get('csrf_token')
if request.method == 'POST':
if not secrets.compare_digest(request.values.get('csrf_token', ''), request.csrf_token):
abort(403, 'CSRF check failed')
@app.route('/')
def index():
return redirect(url_for('item_list'))
@app.route('/oauth2/start')
def oauth2_start():
client = OAuth2Session(
app.config['OAUTH2_CLIENT_ID'],
redirect_uri=url_for('oauth2_callback', _external=True)
)
url, state = client.authorization_url(app.config['OAUTH2_AUTH_URL'])
session.clear()
session['oauth2_state'] = state
return redirect(url)
@app.route('/oauth2/callback')
def oauth2_callback():
oauth2_next = session.get('oauth2_next', '/')
oauth2_state = session.get('oauth2_state')
if not oauth2_state:
session.clear()
return redirect(oauth2_next)
client = OAuth2Session(
app.config['OAUTH2_CLIENT_ID'],
redirect_uri=url_for('oauth2_callback', _external=True),
state=oauth2_state
)
client.fetch_token(
app.config['OAUTH2_TOKEN_URL'],
client_secret=app.config['OAUTH2_CLIENT_SECRET'],
authorization_response=request.url,
verify=(not app.debug)
)
userinfo = client.get(app.config['OAUTH2_USERINFO_URL']).json()
session.clear()
session['auth'] = 'oauth2'
session['auth_user'] = userinfo['nickname']
session['auth_timestamp'] = int(time.time())
session['csrf_token'] = secrets.token_hex(32)
return redirect(oauth2_next)
@app.route('/QRAUTH/<code>')
def qrauth(code):
if not app.config.get('QRAUTH_CODE'):
abort(404)
if not secrets.compare_digest(code, app.config['QRAUTH_CODE']):
abort(404)
session['auth'] = 'qrauth'
session['auth_code'] = code
session['auth_user'] = None
session['auth_timestamp'] = int(time.time())
session['csrf_token'] = secrets.token_hex(32)
return redirect('/')
@app.route('/logout')
def logout():
session.clear()
return redirect('/')
@app.route('/items/')
def item_list():
return render_template('item/list.html', page=Item.query.order_by('name').paginate(per_page=10))
@app.route('/items/add', methods=['GET', 'POST'])
def item_add():
if request.method == 'GET':
return render_template('item/add.html')
item = Item(
name=request.form['name'],
description=request.form.get('description', '')
)
db.session.add(item)
db.session.commit()
return redirect(url_for('item_view', item_id=item.id))
@app.route('/item/<item_id>/')
def item_view(item_id):
return render_template('item/view.html', item=Item.query.get_or_404(item_id))
@app.route('/item/<item_id>/label')
def item_label(item_id):
return render_pdf(render_template('item/label.html', item=Item.query.get_or_404(item_id)))
@app.route('/item/<item_id>/print_label')
def item_print_label(item_id):
item = Item.query.get_or_404(item_id)
resp = render_pdf(render_template('item/label.html', item=item))
print_pdf(resp.data, app.config['PRINTER_URL'])
flash('Print job submitted')
return redirect(url_for('item_view', item_id=item.id))
@app.route('/item/<item_id>/html_label')
def item_html_label(item_id):
return render_template('item/label.html', item=Item.query.get_or_404(item_id))
@app.route('/item/<item_id>/edit', methods=['GET', 'POST'])
def item_edit(item_id):
item = Item.query.get_or_404(item_id)
if request.method == 'GET':
return render_template('item/edit.html', item=item)
item.name = request.form['name']
item.description = request.form['description']
db.session.commit()
return redirect(url_for('item_view', item_id=item.id))
@app.route('/item/<item_id>/', methods=['DELETE'])
def item_delete(item_id):
db.session.delete(Item.query.get_or_404(item_id))
db.session.commit()
return redirect(url_for('item_list'))
@app.route('/item/<item_id>/photo', methods=['POST'])
def item_upload_photo(item_id):
item = Item.query.get_or_404(item_id)
item.photos.append(Photo.from_form(request.files['file']))
db.session.commit()
return redirect(url_for('item_view', item_id=item_id))
@app.route('/item/<item_id>/locate', methods=['GET', 'POST'])
def item_locate(item_id):
item = Item.query.get_or_404(item_id)
if request.method == 'GET':
return render_template('item/locate.html', item=item, all_locations=Location.query.all())
item.location = Location.query.get(request.form['location_id'])
db.session.commit()
return redirect(url_for('item_view', item_id=item_id))
@app.route('/photos/<int:photo_id>')
def photo_show(photo_id):
return Photo.query.get_or_404(photo_id).send()
@app.route('/C/<code>')
def qrcode_url(code):
item = Item.query.filter_by(qr_code=code).first_or_404()
return redirect(url_for('item_view', item_id=item.id))
import secrets
import os
from flask import current_app, send_file
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class Photo(db.Model):
__tablename__ = 'photo'
id = db.Column(db.Integer(), primary_key=True, autoincrement=True)
mimetype = db.Column(db.String(128), nullable=False)
path = db.Column(db.String(128), nullable=True)
@classmethod
def from_form(cls, filestorage):
obj = cls(mimetype='image/jpeg')
db.session.add(obj)
obj.path = f'photos/{obj.id}'
path = os.path.join(current_app.config['UPLOAD_FOLDER'], obj.path)
filestorage.save(path)
return obj
@classmethod
def from_data(cls, data):
obj = cls(mimetype='image/jpeg')
db.session.add(obj)
db.session.flush()
obj.path = f'photos/{obj.id}'
path = os.path.join(current_app.config['UPLOAD_FOLDER'], obj.path)
print(path)
with open(path, 'wb') as f:
f.write(data)
return obj
def send(self):
path = os.path.join(current_app.config['UPLOAD_FOLDER'], self.path)
return send_file(path, mimetype=self.mimetype)
def token_typable():
alphabet = '23456789ABCDEFGHJKLMNPQRSTUVWX' # not '01OIYZ'
return ''.join([secrets.choice(alphabet) for _ in range(5)])
def token_qrfriendly():
alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
return ''.join([secrets.choice(alphabet) for _ in range(16)])
class Location(db.Model):
__tablename__ = 'location'
id = db.Column(db.String(5), primary_key=True, default=token_typable)
name = db.Column(db.String(128), nullable=False)
description = db.Column(db.Text(), nullable=False, server_default='')
parent_id = db.Column(db.String(5), db.ForeignKey('location.id'))
parent = db.relationship('Location')
item_photo = db.Table('item_photo',
db.Column('item_id', db.String(5), db.ForeignKey('item.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True),
db.Column('photo_id', db.Integer(), db.ForeignKey('photo.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True),
)
class Item(db.Model):
__tablename__ = 'item'
id = db.Column(db.String(5), primary_key=True, default=token_typable)
qr_code = db.Column(db.String(128), nullable=False, unique=True, default=token_qrfriendly)
name = db.Column(db.String(128), nullable=False)
description = db.Column(db.Text(), nullable=False, server_default='')
photos = db.relationship('Photo', secondary='item_photo')
location_id = db.Column(db.String(5), db.ForeignKey('location.id'))
location = db.relationship('Location')
This diff is collapsed.
Source diff could not be displayed: it is too large. Options to address this: view the blob.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Source diff could not be displayed: it is too large. Options to address this: view the blob.
This diff is collapsed.
This diff is collapsed.
/*!
* Bootstrap Reboot v5.2.3 (https://getbootstrap.com/)
* Copyright 2011-2022 The Bootstrap Authors
* Copyright 2011-2022 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--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-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg-rgb: 255, 255, 255;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--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-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-bg: #fff;
--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.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-2xl: 2rem;
--bs-border-radius-pill: 50rem;
--bs-link-color: #0d6efd;
--bs-link-hover-color: #0a58ca;
--bs-code-color: #d63384;
--bs-highlight-bg: #fff3cd;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
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: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
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: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.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: var(--bs-link-color);
text-decoration: underline;
}
a:hover {
color: var(--bs-link-hover-color);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
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: 0.5rem;
padding-bottom: 0.5rem;
color: #6c757d;
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: 0.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] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::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;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */
\ No newline at end of file
This diff is collapsed.
/*!
* Bootstrap Reboot v5.2.3 (https://getbootstrap.com/)
* Copyright 2011-2022 The Bootstrap Authors
* Copyright 2011-2022 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--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-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--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-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff;--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.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-2xl:2rem;--bs-border-radius-pill:50rem;--bs-link-color:#0d6efd;--bs-link-hover-color:#0a58ca;--bs-code-color:#d63384;--bs-highlight-bg:#fff3cd}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}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:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:1px solid;opacity:.25}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.1875em;background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:var(--bs-link-color);text-decoration:underline}a:hover{color:var(--bs-link-hover-color)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}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:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,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}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button: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 + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::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}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment