Convert line endings to LF

pull/3/head
Charlotte Som 2022-03-05 14:41:57 +00:00
parent 1e235dd0d1
commit ab9e7598be
12 changed files with 396 additions and 396 deletions

View File

@ -1,12 +1,12 @@
root = true root = true
[*] [*]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
end_of_line = crlf end_of_line = lf
charset = utf-8 charset = utf-8
trim_trailing_whitespace = false trim_trailing_whitespace = false
insert_final_newline = true insert_final_newline = true
[*.rs] [*.rs]
indent_size = 4 indent_size = 4

4
.gitignore vendored
View File

@ -1,2 +1,2 @@
/target /target
/dist /dist

View File

@ -1,38 +1,38 @@
The Charlotte Public License version 0.1 The Charlotte Public License version 0.1
Copyright 2021, Lavender Software (collectively, the "Author" henceforth). Copyright 2021, Lavender Software (collectively, the "Author" henceforth).
This license gives everyone permission to examine, modify, and use this software This license gives everyone permission to examine, modify, and use this software
and the associated documentation (the "Inator"), without patent obstacles, while protecting and the associated documentation (the "Inator"), without patent obstacles, while protecting
the Author and any contributors (the "Composers") from liability. the Author and any contributors (the "Composers") from liability.
Each Composer permits you to examine, modify, utilize, and distribute the Inator Each Composer permits you to examine, modify, utilize, and distribute the Inator
where it would otherwise infringe upon that Composer's copyright or any patent claims that where it would otherwise infringe upon that Composer's copyright or any patent claims that
they hold. they hold.
No contributor may revoke this license, but the Author may choose to release the Inator No contributor may revoke this license, but the Author may choose to release the Inator
(including the contributed works of any other Composer) under a different license. (including the contributed works of any other Composer) under a different license.
You may not use the Inator to accrue revenue without explicit permission from the Author. You may not use the Inator to accrue revenue without explicit permission from the Author.
You may not use the Inator to do Malevolence. If you are You may not use the Inator to do Malevolence. If you are
notified that you have committed a Malevolence instrumented by the Inator, your license is notified that you have committed a Malevolence instrumented by the Inator, your license is
terminated unless you take all practical steps to comply within a reasonable timeframe. terminated unless you take all practical steps to comply within a reasonable timeframe.
The definition of Malevolence is at the discretion of the Author. It may include, but is not The definition of Malevolence is at the discretion of the Author. It may include, but is not
limited to: limited to:
- The promotion of bigotry, including: sexism, transphobia, homophobia, ableism, or the - The promotion of bigotry, including: sexism, transphobia, homophobia, ableism, or the
perpetuation of racial oppression. perpetuation of racial oppression.
- Causing a detriment to public health. - Causing a detriment to public health.
- Instigating political, economic, or corporeal violence. - Instigating political, economic, or corporeal violence.
- Entrenching an empire. - Entrenching an empire.
- Where applicable, use of the Inator without the informed consent of a second party who may - Where applicable, use of the Inator without the informed consent of a second party who may
object to its use. object to its use.
The Inator is provided without any warranty, "as-is". No Composer is liable for any damages The Inator is provided without any warranty, "as-is". No Composer is liable for any damages
related to the Inator. related to the Inator.
In order to receive this license, you must agree to the terms set out in this document. In order to receive this license, you must agree to the terms set out in this document.
This license, authorial attribution, and copyright notice must be distributed with This license, authorial attribution, and copyright notice must be distributed with
any copies or large portions of the Inator. any copies or large portions of the Inator.

View File

@ -1,32 +1,32 @@
# lavender.software # lavender.software
Static site generated using [siru](https://github.com/videogame-hacker/siru) and hosted at [lavender.software](https://lavender.software/). Static site generated using [siru](https://github.com/videogame-hacker/siru) and hosted at [lavender.software](https://lavender.software/).
## Setting Up ## Setting Up
```shell ```shell
$ git clone 'git@lavender.software:lavender/lavender.software.git' $ git clone 'git@lavender.software:lavender/lavender.software.git'
$ cd lavender.software/ $ cd lavender.software/
lavender.software/ $ mkdir dist # or follow instructions in 'Deploying' lavender.software/ $ mkdir dist # or follow instructions in 'Deploying'
lavender.software/ $ cargo run lavender.software/ $ cargo run
... ...
lavender.software/ $ # Built files are in dist/ lavender.software/ $ # Built files are in dist/
``` ```
You may want to `cd dist && python -m http.server` to get a local HTTP server. You may want to `cd dist && python -m http.server` to get a local HTTP server.
## Deploying ## Deploying
**Note:** You don't need to do this unless you're th eone deploying the site to the production environment. **Note:** You don't need to do this unless you're th eone deploying the site to the production environment.
```shell ```shell
$ # To set up, ensure that the 'dist' folder reflects the VPS $ # To set up, ensure that the 'dist' folder reflects the VPS
$ git clone 'root@lavender.software:/srv/http/lavender.software' dist $ git clone 'root@lavender.software:/srv/http/lavender.software' dist
$ $
$ cargo run # Build the site $ cargo run # Build the site
$ cd dist/ $ cd dist/
dist/ $ git add -A . && git commit -m "Deploying: $(date)" dist/ $ git add -A . && git commit -m "Deploying: $(date)"
dist/ $ git pull --rebase dist/ $ git pull --rebase
dist/ $ git push dist/ $ git push
dist/ $ # Your changes should now be live at lavender.software dist/ $ # Your changes should now be live at lavender.software
``` ```

View File

@ -1,3 +1,3 @@
fn main() { fn main() {
println!("cargo:rerun-if-changed=build_src"); println!("cargo:rerun-if-changed=build_src");
} }

View File

@ -1,24 +1,24 @@
use std::{fs, path::Path}; use std::{fs, path::Path};
use crate::*; use crate::*;
fn copy_dir_recursive(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> { fn copy_dir_recursive(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
fs::create_dir_all(&dst)?; fs::create_dir_all(&dst)?;
for entry in fs::read_dir(src)? { for entry in fs::read_dir(src)? {
let entry = entry?; let entry = entry?;
let ty = entry.file_type()?; let ty = entry.file_type()?;
if ty.is_dir() { if ty.is_dir() {
copy_dir_recursive(entry.path(), dst.as_ref().join(entry.file_name()))?; copy_dir_recursive(entry.path(), dst.as_ref().join(entry.file_name()))?;
} else { } else {
fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?; fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
} }
} }
Ok(()) Ok(())
} }
pub fn copy_assets(ctx: &BuildContext) -> Result<()> { pub fn copy_assets(ctx: &BuildContext) -> Result<()> {
log_info("Copying assets…"); log_info("Copying assets…");
copy_dir_recursive(ctx.source_dir.join("assets"), ctx.output_dir.join("assets"))?; copy_dir_recursive(ctx.source_dir.join("assets"), ctx.output_dir.join("assets"))?;
Ok(()) Ok(())
} }

View File

@ -1,65 +1,65 @@
use siru::prelude::*; use siru::prelude::*;
use std::{convert::TryInto, path::PathBuf, sync::Arc, time::Duration}; use std::{convert::TryInto, path::PathBuf, sync::Arc, time::Duration};
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>; type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
mod assets; mod assets;
mod main_page; mod main_page;
pub struct BuildContext { pub struct BuildContext {
source_dir: PathBuf, source_dir: PathBuf,
output_dir: PathBuf, output_dir: PathBuf,
write_pipeline: WritePipeline, write_pipeline: WritePipeline,
} }
impl SiruFS for BuildContext { impl SiruFS for BuildContext {
fn get_source_dir(&self) -> &PathBuf { fn get_source_dir(&self) -> &PathBuf {
&self.source_dir &self.source_dir
} }
fn get_output_dir(&self) -> &PathBuf { fn get_output_dir(&self) -> &PathBuf {
&self.output_dir &self.output_dir
} }
fn get_write_pipeline(&self) -> &WritePipeline { fn get_write_pipeline(&self) -> &WritePipeline {
&self.write_pipeline &self.write_pipeline
} }
} }
fn build() { fn build() {
let ctx = BuildContext { let ctx = BuildContext {
source_dir: "src".try_into().unwrap(), source_dir: "src".try_into().unwrap(),
output_dir: "dist".try_into().unwrap(), output_dir: "dist".try_into().unwrap(),
write_pipeline: WritePipeline::new(), write_pipeline: WritePipeline::new(),
}; };
let ctx = Arc::new(ctx); let ctx = Arc::new(ctx);
[main_page::main_page, assets::copy_assets] [main_page::main_page, assets::copy_assets]
.iter() .iter()
.map(|f| { .map(|f| {
let ctx = Arc::clone(&ctx); let ctx = Arc::clone(&ctx);
std::thread::spawn(move || f(&ctx).unwrap()) std::thread::spawn(move || f(&ctx).unwrap())
}) })
.for_each(|t| t.join().unwrap()); .for_each(|t| t.join().unwrap());
} }
fn main() { fn main() {
build(); build();
if matches!(std::env::args().nth(1).as_deref(), Some("watch")) { if matches!(std::env::args().nth(1).as_deref(), Some("watch")) {
use notify::*; use notify::*;
let (tx, rx) = std::sync::mpsc::channel(); let (tx, rx) = std::sync::mpsc::channel();
let mut watcher = watcher(tx, Duration::from_millis(100)).unwrap(); let mut watcher = watcher(tx, Duration::from_millis(100)).unwrap();
watcher.watch("./src", RecursiveMode::Recursive).unwrap(); watcher.watch("./src", RecursiveMode::Recursive).unwrap();
loop { loop {
match rx.recv() { match rx.recv() {
Ok(_) => build(), Ok(_) => build(),
Err(_) => break, Err(_) => break,
} }
} }
} }
} }

View File

@ -1,58 +1,58 @@
use crate::*; use crate::*;
struct Member { struct Member {
name: String, name: String,
website: String, website: String,
title: String, title: String,
} }
impl From<&(&str, &str, &str)> for Member { impl From<&(&str, &str, &str)> for Member {
fn from(tuple: &(&str, &str, &str)) -> Self { fn from(tuple: &(&str, &str, &str)) -> Self {
Member { Member {
name: tuple.0.to_string(), name: tuple.0.to_string(),
website: tuple.1.to_string(), website: tuple.1.to_string(),
title: tuple.2.to_string(), title: tuple.2.to_string(),
} }
} }
} }
#[derive(Template)] #[derive(Template)]
#[template(path = "main_page.html.j2")] #[template(path = "main_page.html.j2")]
struct MainPageTemplate { struct MainPageTemplate {
members: Vec<Member>, members: Vec<Member>,
} }
pub fn main_page(ctx: &BuildContext) -> Result<()> { pub fn main_page(ctx: &BuildContext) -> Result<()> {
log_info("Rendering main page…"); log_info("Rendering main page…");
let members = [ let members = [
("charlotte som", "https://som.codes/", "founder"), ("charlotte som", "https://som.codes/", "founder"),
("agatha rose", "https://agatharose.dev/", "meow"), ("agatha rose", "https://agatharose.dev/", "meow"),
("maya", "https://1312.gay/", "chief director of maya"), ("maya", "https://1312.gay/", "chief director of maya"),
( (
"Luna Lulu", "Luna Lulu",
"https://lunaisa.dev", "https://lunaisa.dev",
"critically acclaimed website maker", "critically acclaimed website maker",
), ),
( (
"annie versario", "annie versario",
"https://annie.kitty.lgbt", "https://annie.kitty.lgbt",
"regional marquee technician", "regional marquee technician",
), ),
( (
"The System", "The System",
"https://the-system.eu.org", "https://the-system.eu.org",
"lead systems specialist", "lead systems specialist",
), ),
]; ];
ctx.write( ctx.write(
"index.html", "index.html",
MainPageTemplate { MainPageTemplate {
members: members.iter().map(|x| x.into()).collect(), members: members.iter().map(|x| x.into()).collect(),
} }
.render()?, .render()?,
)?; )?;
Ok(()) Ok(())
} }

View File

@ -1,3 +1,3 @@
/* confettijs.org MIT */ const Confetti = (() => { "use strict"; const e = 10; let t, o, n = 75, i = 25, r = 1, s = !1, l = !0, a = [], d = (new Date).getTime(); function p(e) { if (t = document.createElement("canvas"), o = t.getContext("2d"), t.width = 2 * window.innerWidth, t.height = 2 * window.innerHeight, t.style.position = "fixed", t.style.top = 0, t.style.left = 0, t.style.width = "calc(100%)", t.style.height = "calc(100%)", t.style.margin = 0, t.style.padding = 0, t.style.zIndex = 999999999, t.style.pointerEvents = "none", document.body.appendChild(t), null != e) { let t = document.getElementById(e); null != t && t.addEventListener("click", e => { !function (e, t) { let o = []; for (let i = 0; i < n; i++)o.push(c(e, t)); a.push({ particles: o }) }(2 * e.clientX, 2 * e.clientY), l && (e.target.style.visibility = "hidden") }) } window.addEventListener("resize", () => { t.width = 2 * window.innerWidth, t.height = 2 * window.innerHeight }) } function y(e) { return e.pos.y - 2 * e.size.x > 2 * window.innerHeight } function c(e, t) { let o = (16 * Math.random() + 4) * r, n = (4 * Math.random() + 4) * r; return { pos: { x: e - o / 2, y: t - n / 2 }, vel: h(), size: { x: o, y: n }, rotation: 360 * Math.random(), rotation_speed: 10 * (Math.random() - .5), hue: 360 * Math.random(), opacity: 100, lifetime: Math.random() + .25 } } function h() { let e = Math.random() - .5, t = Math.random() - .7, o = Math.sqrt(e * e + t * t); return t /= o, { x: (e /= o) * (Math.random() * i), y: t * (Math.random() * i) } } return p.prototype.setCount = (e => { "number" == typeof e ? n = e : console.error("[ERROR] Confetti.setCount() - Input needs to be of type 'number'") }), p.prototype.setPower = (e => { "number" == typeof e ? i = e : console.error("[ERROR] Confetti.setPower() - Input needs to be of type 'number'") }), p.prototype.setSize = (e => { "number" == typeof e ? r = e : console.error("[ERROR] Confetti.setSize() - Input needs to be of type 'number'") }), p.prototype.setFade = (e => { "boolean" == typeof e ? s = e : console.error("[ERROR] Confetti.setFade() - Input needs to be of type 'boolean'") }), p.prototype.destroyTarget = (e => { "boolean" == typeof e ? l = e : console.error("[ERROR] Confetti.destroyTarget() - Input needs to be of type 'boolean'") }), window.requestAnimationFrame(function t(n) { let i = (n - d) / 1e3; d = n; for (let t = a.length - 1; t >= 0; t--) { let o = a[t]; for (let t = o.particles.length - 1; t >= 0; t--) { let n = o.particles[t]; n.vel.y += e * (n.size.y / (10 * r)) * i, n.vel.x += 25 * (Math.random() - .5) * i, n.vel.x *= .98, n.vel.y *= .98, n.pos.x += n.vel.x, n.pos.y += n.vel.y, n.rotation += n.rotation_speed, s && (n.opacity -= n.lifetime), y(n) && o.particles.splice(t, 1) } 0 == o.particles.length && a.splice(t, 1) } !function () { o.clearRect(0, 0, 2 * window.innerWidth, 2 * window.innerHeight); for (const d of a) for (const a of d.particles) e = a.pos.x, t = a.pos.y, n = a.size.x, i = a.size.y, r = a.rotation, s = a.hue, l = a.opacity, o.save(), o.beginPath(), o.translate(e + n / 2, t + i / 2), o.rotate(r * Math.PI / 180), o.rect(-n / 2, -i / 2, n, i), o.fillStyle = `hsla(275deg, ${s}%, ${s / 3.6}%, ${l}%)`, o.fill(), o.restore(); var e, t, n, i, r, s, l }(), window.requestAnimationFrame(t) }), p })(); /* confettijs.org MIT */ const Confetti = (() => { "use strict"; const e = 10; let t, o, n = 75, i = 25, r = 1, s = !1, l = !0, a = [], d = (new Date).getTime(); function p(e) { if (t = document.createElement("canvas"), o = t.getContext("2d"), t.width = 2 * window.innerWidth, t.height = 2 * window.innerHeight, t.style.position = "fixed", t.style.top = 0, t.style.left = 0, t.style.width = "calc(100%)", t.style.height = "calc(100%)", t.style.margin = 0, t.style.padding = 0, t.style.zIndex = 999999999, t.style.pointerEvents = "none", document.body.appendChild(t), null != e) { let t = document.getElementById(e); null != t && t.addEventListener("click", e => { !function (e, t) { let o = []; for (let i = 0; i < n; i++)o.push(c(e, t)); a.push({ particles: o }) }(2 * e.clientX, 2 * e.clientY), l && (e.target.style.visibility = "hidden") }) } window.addEventListener("resize", () => { t.width = 2 * window.innerWidth, t.height = 2 * window.innerHeight }) } function y(e) { return e.pos.y - 2 * e.size.x > 2 * window.innerHeight } function c(e, t) { let o = (16 * Math.random() + 4) * r, n = (4 * Math.random() + 4) * r; return { pos: { x: e - o / 2, y: t - n / 2 }, vel: h(), size: { x: o, y: n }, rotation: 360 * Math.random(), rotation_speed: 10 * (Math.random() - .5), hue: 360 * Math.random(), opacity: 100, lifetime: Math.random() + .25 } } function h() { let e = Math.random() - .5, t = Math.random() - .7, o = Math.sqrt(e * e + t * t); return t /= o, { x: (e /= o) * (Math.random() * i), y: t * (Math.random() * i) } } return p.prototype.setCount = (e => { "number" == typeof e ? n = e : console.error("[ERROR] Confetti.setCount() - Input needs to be of type 'number'") }), p.prototype.setPower = (e => { "number" == typeof e ? i = e : console.error("[ERROR] Confetti.setPower() - Input needs to be of type 'number'") }), p.prototype.setSize = (e => { "number" == typeof e ? r = e : console.error("[ERROR] Confetti.setSize() - Input needs to be of type 'number'") }), p.prototype.setFade = (e => { "boolean" == typeof e ? s = e : console.error("[ERROR] Confetti.setFade() - Input needs to be of type 'boolean'") }), p.prototype.destroyTarget = (e => { "boolean" == typeof e ? l = e : console.error("[ERROR] Confetti.destroyTarget() - Input needs to be of type 'boolean'") }), window.requestAnimationFrame(function t(n) { let i = (n - d) / 1e3; d = n; for (let t = a.length - 1; t >= 0; t--) { let o = a[t]; for (let t = o.particles.length - 1; t >= 0; t--) { let n = o.particles[t]; n.vel.y += e * (n.size.y / (10 * r)) * i, n.vel.x += 25 * (Math.random() - .5) * i, n.vel.x *= .98, n.vel.y *= .98, n.pos.x += n.vel.x, n.pos.y += n.vel.y, n.rotation += n.rotation_speed, s && (n.opacity -= n.lifetime), y(n) && o.particles.splice(t, 1) } 0 == o.particles.length && a.splice(t, 1) } !function () { o.clearRect(0, 0, 2 * window.innerWidth, 2 * window.innerHeight); for (const d of a) for (const a of d.particles) e = a.pos.x, t = a.pos.y, n = a.size.x, i = a.size.y, r = a.rotation, s = a.hue, l = a.opacity, o.save(), o.beginPath(), o.translate(e + n / 2, t + i / 2), o.rotate(r * Math.PI / 180), o.rect(-n / 2, -i / 2, n, i), o.fillStyle = `hsla(275deg, ${s}%, ${s / 3.6}%, ${l}%)`, o.fill(), o.restore(); var e, t, n, i, r, s, l }(), window.requestAnimationFrame(t) }), p })();
const c = new Confetti("purple"); const c = new Confetti("purple");
c.destroyTarget(false); c.destroyTarget(false);

View File

@ -1,88 +1,88 @@
:root { :root {
--bg: rgb(28, 23, 36); --bg: rgb(28, 23, 36);
--fg: rgb(234, 234, 248); --fg: rgb(234, 234, 248);
--accent: hsl(275, 57%, 68%); --accent: hsl(275, 57%, 68%);
} }
html { html {
background-color: var(--bg); background-color: var(--bg);
color: var(--fg); color: var(--fg);
font-size: 1.125em; font-size: 1.125em;
font-family: sans-serif; font-family: sans-serif;
} }
a { a {
color: var(--accent); color: var(--accent);
text-decoration: none; text-decoration: none;
border-bottom: 1px solid var(--accent); border-bottom: 1px solid var(--accent);
} }
html, html,
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
body { body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100vh; min-height: 100vh;
} }
header, header,
main, main,
footer { footer {
width: 100%; width: 100%;
max-width: 90ch; max-width: 90ch;
margin: 0 auto; margin: 0 auto;
padding: 1em; padding: 1em;
} }
main { main {
flex: 1; flex: 1;
margin-bottom: 2em; margin-bottom: 2em;
} }
header { header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
} }
header img { header img {
display: inline-block; display: inline-block;
margin-right: 2em; margin-right: 2em;
height: 8em; height: 8em;
} }
#purple { #purple {
font-style: normal; font-style: normal;
color: var(--bg); color: var(--bg);
background-color: var(--accent); background-color: var(--accent);
border-radius: 2px; border-radius: 2px;
padding: 0.25em; padding: 0.25em;
} }
li { li {
line-height: 1.6em; line-height: 1.6em;
} }
footer nav ul { footer nav ul {
list-style: none; list-style: none;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
footer nav ul > li { footer nav ul > li {
display: inline; display: inline;
line-height: 1rem; line-height: 1rem;
padding-bottom: 0.15em; padding-bottom: 0.15em;
} }
footer nav ul > li + li { footer nav ul > li + li {
border-inline-start: 1px solid var(--fg); border-inline-start: 1px solid var(--fg);
padding-inline-start: 1ch; padding-inline-start: 1ch;
margin-inline-start: 1ch; margin-inline-start: 1ch;
} }

View File

@ -1,9 +1,9 @@
<footer> <footer>
<nav> <nav>
<ul> <ul>
<li><strong class="brand">lavender software ltd</strong></li> <li><strong class="brand">lavender software ltd</strong></li>
<li><a href="https://git.lavender.software/lavender/lavender.software">source code</a></li> <li><a href="https://git.lavender.software/lavender/lavender.software">source code</a></li>
<!-- <li><a href="/about/">about us</a></li> --> <!-- <li><a href="/about/">about us</a></li> -->
</ul> </ul>
</nav> </nav>
</footer> </footer>

View File

@ -1,62 +1,62 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>lavender software | digital product studio</title> <title>lavender software | digital product studio</title>
<link rel="stylesheet" href="/assets/styles.css"> <link rel="stylesheet" href="/assets/styles.css">
</head> </head>
<body> <body>
<header> <header>
<img alt="Lavender Logo" src="/assets/logo.svg"> <img alt="Lavender Logo" src="/assets/logo.svg">
<div> <div>
<h1>lavender software</h1> <h1>lavender software</h1>
<p>the kind of software that we make is … <em id="purple">purple</em></p> <p>the kind of software that we make is … <em id="purple">purple</em></p>
</div> </div>
</header> </header>
<main> <main>
<section> <section>
<h2>projects</h2> <h2>projects</h2>
<ul> <ul>
<li><a href="https://git.lain.faith/videogame-hacker/discord-css-injector">lavender cord</a> - a theming platform for Discord</li> <li><a href="https://git.lain.faith/videogame-hacker/discord-css-injector">lavender cord</a> - a theming platform for Discord</li>
<li>more soon!</li> <li>more soon!</li>
</ul> </ul>
</section> </section>
<!-- <!--
<section> <section>
<h2>who are we?</h2> <h2>who are we?</h2>
<ul> <ul>
{% for member in members %} {% for member in members %}
<li> <li>
<a href="{{ member.website }}">{{ member.name }}</a> - {{ member.title }} <a href="{{ member.website }}">{{ member.name }}</a> - {{ member.title }}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
</section> --> </section> -->
<section> <section>
<h2>services</h2> <h2>services</h2>
<p>we offer:</p> <p>we offer:</p>
<ul> <ul>
<li>individual software consulting</li> <li>individual software consulting</li>
<li>system operations for-hire</li> <li>system operations for-hire</li>
<li>contractual project work</li> <li>contractual project work</li>
</ul> </ul>
</section> </section>
</main> </main>
{% include "_footer.html.j2" %} {% include "_footer.html.j2" %}
<script async src="/assets/main_page/confetti.js"></script> <script async src="/assets/main_page/confetti.js"></script>
</body> </body>
</html> </html>