diff --git a/_includes/footer.pug b/_includes/footer.pug index cbb439a..7e43bea 100644 --- a/_includes/footer.pug +++ b/_includes/footer.pug @@ -1,2 +1,2 @@ footer - p Vlad Faust © 2020 + p Vlad Faust © 1996-present diff --git a/_includes/head.pug b/_includes/head.pug index ccb3fcf..c234857 100644 --- a/_includes/head.pug +++ b/_includes/head.pug @@ -3,7 +3,7 @@ meta(charset='UTF-8') meta(name='viewport', content='width=device-width, initial-scale=1.0') -title= title ? (titleNoAppend ? title : (title + " | Vlad Faust Blog")) : "Vlad Faust Blog" +title= title ? (titleNoAppend ? title : (title + " - Vlad Faust Blog")) : "Vlad Faust Blog" meta(name='description', content=description || excerpt) meta(name='robots' content="index,follow") meta(name='generator' content='Eleventy') diff --git a/_includes/header.pug b/_includes/header.pug index 3fdf0a5..5c301f7 100644 --- a/_includes/header.pug +++ b/_includes/header.pug @@ -4,15 +4,19 @@ header div.text p | Hello, my name is  - a(href='/') Vlad Faust + a(href='/') Vladislav Faust | . p.tablet-hide - | I'm a passionate developer currently working on  - a(href='/posts/2020-08-20-the-onyx-programming-language') my own programming language + | I am a jack of anything IT, master of some; see my  + a(href='/cv') CV | . p.tablet-hide - | You can follow me on  + | You can contact me via  a(href='https://twitter.com/vladfaust') Twitter - | and  + | ,  a(href='https://github.com/vladfaust') GitHub - | and also send nudes to mail at vladfaust.com + | or  + a(href='https://t.me/vladfaust') Telegram + | , or even e-mail me directly at  + i hey at vladfaust.com + | . diff --git a/_includes/posts.pug b/_includes/posts.pug new file mode 100644 index 0000000..6d62753 --- /dev/null +++ b/_includes/posts.pug @@ -0,0 +1,5 @@ +ul.posts + each post in [...collections.post].reverse().filter((post) => !post.data.hidden && post.date <= Date.now()) + li + span.date= post.date.toDateString() + a.title(href=post.url)= post.data.title diff --git a/_includes/projects.pug b/_includes/projects.pug new file mode 100644 index 0000000..933b471 --- /dev/null +++ b/_includes/projects.pug @@ -0,0 +1,357 @@ +h3 Onyx, the language (2020-present) +small.annotation + | Onyx is a novel programming language I'm working on. + +ul.projects + li.project-card + a.title(href="https://nxsf.org") NXSF.org + p.desc + | The Onyx Software Foundation website ( + a(href="https://github.com/nxsf/nxsf.github.io") source + | ). + div.stack + span Open source + + li.project-card + a.title(href="https://nxsf.org/onyx/") Onyx standard + p.desc + | An ongoing attempt to standardize the Onyx language ( + a(href="https://github.com/nxsf/onyx") source + | ). + div.stack + span Open source + + li.project-card + a.title(href="https://ref.nx.fancysoft.xyz/") Onyx reference + p.desc + | An easier-to-understand Onyx language introduction ( + a(href="https://github.com/fancysofthq/onyx-ref") source + | ). + div.stack + span Open source + + li.project-card + a.title(href="https://github.com/onyxlang/onyx") Onyx implementation + p.desc An ongoing attempt to implement an Onyx language compiler. + div.stack + span Open source + span C++ + span LLVM + span Zig + span Typescript + +h3 Crystal-related projects (2018-2020) +small.annotation + a(href="https://crystal-lang.org") Crystal + | is a Ruby-inspired programming language, but compiled. + +ul.projects + li.project-card + span.title + a(href="https://onyxframework.com/") Onyx framework + | (2020) + p.desc + | A web framework with  + a(href="https://github.com/onyxframework/http") HTTP + | ,  + a(href="https://github.com/onyxframework/sql") SQL ORM + | and  + a(href="https://github.com/onyxframework/eda") EDA + | modules, fancy website and rich documentation. + div.stack + span Open source + span Product design + span Crystal + span SQL + span HTTP + span Event-driven architecture + span VueJS + span SASS + + li.project-card + span.title + a(href="https://github.com/crystaljobs") Crystal Jobs + | (2018) + p.desc + | A now defunct Crystal jobs board ( + a(href="https://crystaljobs-staging.netlify.app/") frontend preview + | ). + div.stack + span Open source + span Product design + span Crystal + span VueJS + span SASS + + li.project-card + span.title + a(href="https://github.com/vladfaust/crystalworld") CrystalWorld + | (2019) + p.desc + | A  + a(href="https://github.com/gothinkster/realworld") RealWorld + | implementation in Crystal and Onyx the web framework. + div.stack + span Open source + span Crystal + span SQLite + +h4 Crystal shards (2018-2020) +small.annotation + | A  + i shard + | is a Crystal code package. + +ul.projects + li.project-card + a.title(href="https://github.com/vladfaust/time_format.cr") TimeFormat + p.desc Time spans formatting made simple. + div.stack + span Open source + span Crystal + + li.project-card + a.title(href="https://github.com/vladfaust/i18n.cr") I18n + p.desc Internationalization shard. + div.stack + span Open source + span Crystal + + li.project-card + a.title(href="https://github.com/vladfaust/http-multiserver.cr") HTTP::Multiserver + p.desc Mount multiple web applications. + div.stack + span Open source + span Crystal + span HTTP + + li.project-card + a.title(href="https://github.com/vladfaust/tarantool.cr") Tarantool + p.desc + | A  + a(href="https://www.tarantool.io/ru/") Tarantool + | database driver. + div.stack + span Open source + span Crystal + span TCP + + li.project-card + a.title(href="https://github.com/vladfaust/cake-bake") Cake-Bake + p.desc Bake Cakefile into native Crystal code. + div.stack + span Open source + span Crystal + + li.project-card + a.title(href="https://github.com/vladfaust/callbacks.cr") Callbacks + p.desc Expressive callbacks module. + div.stack + span Open source + span Crystal + + li.project-card + a.title(href="https://github.com/vladfaust/validations.cr") Validations + p.desc Validations module. + div.stack + span Open source + span Crystal + + li.project-card + a.title(href="https://github.com/vladfaust/mini_redis") MiniRedis + p.desc A light-weight low-level Redis client. + div.stack + span Open source + span Crystal + span TCP + + li.project-card + a.title(href="https://github.com/vladfaust/http-params-serializable") HTTP::Params::Serializable + p.desc The HTTP params parsing module. + div.stack + span Open source + span Crystal + + li.project-card + a.title(href="https://github.com/vladfaust/migrate.cr") Migrate + p.desc A database migration solution. + div.stack + span Open source + span Crystal + span SQL + + li.project-card + a.title(href="https://github.com/vladfaust/stripe.cr") Stripe + p.desc Stripe API wrapper. + div.stack + span Open source + span Crystal + span API wrapper + + li.project-card + a.title(href="https://github.com/vladfaust/tele.cr") Tele + p.desc A convenient Telegram Bot framework. + div.stack + span Open source + span Crystal + span Telegram bot + + li.project-card + a.title(href="https://github.com/vladfaust/tele-broadcast.cr") Tele::Broadcast + p.desc Broadcasting for Tele. + div.stack + span Open source + span Crystal + span Telegram bot + span Redis + +h3 Ruby gems (2015-2017) +ul.projects + li.project-card + a.title(href="https://github.com/vladfaust/jbuilder-json_api") Jbuilder::JsonApi + p.desc Jbuilder meets jsonapi.org specifications. + div.stack + span Open source + span Ruby + + li.project-card + a.title(href="https://github.com/vladfaust/asset_pipeline") AssetPipeline + p.desc Add Sprockets based Asset Pipeline to your Ruby applications. + div.stack + span Open source + span Ruby on Rails + + li.project-card + a.title(href="https://github.com/vladfaust/bitcoinpay-client") Bitcoinpay + p.desc Bitcoinpay.com API client for Ruby. + div.stack + span Open source + span Ruby + span API wrapper + span Crypto + + li.project-card + a.title(href="https://github.com/vladfaust/blockchain-api") Blockchain::Api + p.desc A blockchain.info API wrapper. + div.stack + span Open source + span Ruby + span API wrapper + span Crypto + +h3 Other projects +ul.projects + li.project-card(style="background-image:url(/public/img/projects/jumpin_sweeties.png); background-repeat: no-repeat; background-position-x: calc(100% + 5rem); background-size: 15rem;") + span.title + a(href="https://xgm.guru/p/sweeties") Jumpin Sweeties + | (2015) + p.desc A casual mobile game. + div.stack + span GameDev + span Java + span LibGDX + + li.project-card + span.title + a(href="https://parrotfarm.herokuapp.com/") Parrot Farm + | (2016) + p.desc + | A showcase project to demonstrate my skills at those times + small , hosted on Heroku. + div.stack + span Open source + span Ruby on Rails + span AngularJS + + li.project-card + span.title + a(href="https://nostalgic-hopper-8a1414.netlify.app/") Coinpricebet + | (2018) + p.desc + | A Stellar-based betting on crypto coin prices, also  + a(href="https://loving-mahavira-537556.netlify.app/") this + | version. + div.stack + span Product design + span Crypto + span VueJS + + li.project-card + span.title + a(href="https://heuristic-wozniak-28f7b6.netlify.app/") AssetFD + | (2018) + p.desc An unfinished decentralized exchange interface based on Stellar. + div.stack + span Crypto + span VueJS + + li.project-card + span.title + a(href="https://github.com/soundmemes") Soundmemes Bot + | (2016-2018) + p.desc A now defunct Telegram bot to post sound memes in chats. + div.stack + span Open source + span Product design + span Telegram bot + span Ruby + span Crystal + span PostgreSQL + span Redis + + li.project-card + span.title + a(href="https://telegra.ph/CashbackBot-09-16") Cashback Bot + | (2017) + p.desc A now defunct Telegram bot to earn cashback from purchases. + div.stack + span Product design + span Telegram bot + span Ruby + span GraphQL + span API wrapper + span PostgreSQL + span Redis + + li.project-card + span.title + a(href="https://www.dropbox.com/sh/wpqjtnw0avw6278/AAC55zKcrbVzFmL12xIT4rK2a?dl=0") Expense Manager 2 + | (2015) + p.desc A collaborative attempt to improve an existing app. + div.stack + span Product design + span Java + span Android + + li.project-card + span.title + a(href="https://www.dropbox.com/sh/0heutamg923ohx5/AADmFkWh06vkM3wDsR0P6g_ha?dl=0") Cycler + | (2014) + p.desc An attempt to create a sleep cycle Android application. + div.stack + span Product design + span Java + span Android + + li.project-card + span.title + a(href="https://www.dropbox.com/s/xc8kjswj4fb0m0z/presentation.pdf?dl=0") Anogram + | (2014) + p.desc A naive attempt to create an anonymous social network. + div.stack + span Product design + span iOS + span PHP + + li.project-card + span.title + a(href="https://xgm.guru/p/ufs/index") UFS Arena + | (2012) + p.desc + | A Warcraft III™ custom map ( + a(href="https://www.dropbox.com/s/odxeaq62jaqvcdc/UFS.w3x?dl=0") source + | ). + dev.stack + span Product design + span GameDev + span cJASS diff --git a/cv.pug b/cv.pug new file mode 100644 index 0000000..ac6fea9 --- /dev/null +++ b/cv.pug @@ -0,0 +1,38 @@ +--- +eleventyExcludeFromCollections: true +title: "CV" +description: "Curriculum vitae" +cover: /public/img/me.jpg +--- + +doctype html +html(lang='en') + head + include /head.pug + + //- Styles + link(rel='stylesheet' href='/public/styles/cv.css') + link(rel='stylesheet' href='/public/styles/posts.css') + link(rel='stylesheet' href='/public/styles/projects.css') + body + include /header.pug + + main + section#about + h2 Curriculum vitae + p Throughout my 10+ years of experience in Information Technology, I've tried everything from compiling COFF after LLVM lowering to designing a resilient ultra-scalable fleet of FAAS workers. Everything from Substrate and IPFS to event-driven stock price prediction. + p Once a technology is understood by me, it becomes boring. It is like surfing on the waves of technology trends, on the surface of the greatest whirlpool of singularity. By understanding I mean not entirely known, e.g. all quirks of the hell-born Rust code, no. Understanding means fundamental, decomposed knowledge enough to build a solution at any scale. + p For me, it is not about thoroughly learning API of yet another web framework, it is instead getting hands on the set of tooling a technology offers, so that I may choose the best framework suitable for the problem. If I were to compile my 10+ years of experience into one line, that would be the following: + big Right tooling is paramount. + p In the very end what I really want is to exercise human love at its fullness. No technology brings me closer to that desire. My extra-wide experience does, though. I am currently available for short-term projects and/or consultancy services; almost any stack. Find my contacts in the header. + p Sincerely,
Vlad. + + section#posts + h2 Recent blog posts + include /posts.pug + + section#projects + h2 Project list + include /projects.pug + + include /footer.pug diff --git a/index.pug b/index.pug index cc763dc..169fa01 100644 --- a/index.pug +++ b/index.pug @@ -1,8 +1,8 @@ --- eleventyExcludeFromCollections: true -title: "Vlad Faust Blog" +title: "Vlad Faust" titleNoAppend: true -description: "Vlad Faust's personal blog" +description: "My personal website" cover: /public/img/me.jpg --- @@ -12,23 +12,19 @@ html(lang='en') include /head.pug //- Styles - link(rel='stylesheet', href='/public/styles/index.css') + link(rel='stylesheet' href='/public/styles/index.css') + link(rel='stylesheet' href='/public/styles/posts.css') + link(rel='stylesheet' href='/public/styles/projects.css') body include /header.pug main - h2 Articles - ul.posts - each post in collections.post.reverse().filter((post) => !post.data.hidden) - li - span.date= post.date.toDateString() - a.title(href=post.url)= post.data.title + section#posts + h2 Recent blog posts + include /posts.pug - h2 Notes - ul.notes - each note in collections.note.reverse().filter((note) => !note.data.hidden) - li - span.date= note.date.toDateString() - a.title(href=note.url)= note.data.title + section#projects + h2 Project list + include /projects.pug include /footer.pug diff --git a/notes/.gitignore b/notes/.gitignore deleted file mode 100644 index 29b54d4..0000000 --- a/notes/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.html -*.11tydata.json diff --git a/notes/2020-09-11-exporting-from-onyx.adoc b/notes/2020-09-11-exporting-from-onyx.adoc deleted file mode 100644 index ce89835..0000000 --- a/notes/2020-09-11-exporting-from-onyx.adoc +++ /dev/null @@ -1,143 +0,0 @@ ---- -title: Exporting from Onyx ---- - -:source-highlighter: highlightjs - -In Onyx, you may `export` a C entity from within an Onyx source file. -Later in the Onyx context, the entity is treated as if it was imported directly from a C header. - -Unlike in raw C headers, an `export` statement is written in some mix of C with Onyx, allowing Onyx annotations, Onyx macros and having function bodies written in Onyx. - -Normally, an `export` statement uses the ISO C syntax, and C vendor extensions support is undefined. -It is recommended to make use of Onyx annotations instead, which may be converted in accordance with a target vendor's semantics. -To achieve that, an Onyx implementation should maintain some sort of mapping between vendor C extensions and according Onyx semantics. - -For example, FNX supports GCC extensions. -Upon generating a header file from Onyx source, the C vendor option (`-c[-w]`) is respected, and Onyx annotations are converted to the vendor's. - -==== -.main.nx -``` -# A struct comment. -@[Pack] -export struct struct_t { - // A variable comment. - @[Align<8>] - int foo; - - double bar; -}; -``` - -After running `fnx doc -fh -c[-wgnu] main.nx`: - -.main.h -```c -// A struct comment. -struct __attribute__ ((__packed__)) struct_t { - // My comment. - int foo __attribute__ ((aligned (8))); - - double bar; -} -``` -==== - -A C macro is treated as a freestanding entity, hence requiring a separate `export` entity per macro, for example: - -==== -.main.nx -```nx -export #ifdef __GNUC__ -@[AlwaysInline] -export int foo(); -export #elif _MSC_VER -@[AlwaysInline] -export double foo(); -export #endif -``` - -After running `fnx doc -fh -c[-wgnu] main.nx`: - -.main.h -```c -#ifdef __GNUC__ -__attribute__((always_inline)) -int foo(); -#elif _MSC_VER -__attribute__((always_inline)) -double foo(); -#endif -``` - -Note that both annotations have expanded to GCC attributes regardless of the `_MSC_VER` macro, because the behaviour is controlled by the `-c[-w]` option. -To solve that, you either want to use explicit C annotations: - -.main.nx -```nx -export #ifdef __GNUC__ -export __attribute__((always_inline)) int foo(); -export #elif _MSC_VER -export __forceinline double foo(); -export #endif -``` - -Or expand them in macros (which is equivalent to writing them explicitly): - -.main.nx -```nx -macro attr(vendor) - {{ nx.id["AlwaysInline"]:c(vendor) }} -end - -export #ifdef __GNUC__ -export @attr("gnu") int foo(); -export #elif _MSC_VER -export @attr("msvc") double foo(); -export #endif -``` -==== - -Sometimes it may become cumbersome to have a separate `export` statement per entity, like in the examples above. -To deal with that, Onyx allows to export entire blocks in the same C-NX syntax. -The same rules apply. - -==== -.main.nx -```nx -export { -#ifdef __GNUC__ - @[AlwaysInline] - int foo(); -#elif _MSC_VER - @[AlwaysInline] - double foo(); -#endif - - // A C comment - @[NoInline] - void main() { - # Some Onyx code - } -} -``` - -After running `fnx doc -fh -c[-wgnu] main.nx`: - -.main.h -```c -#ifdef __GNUC__ -__attribute__((always_inline)) -int foo(); -#elif _MSC_VER -__attribute__((always_inline)) -double foo(); -#endif - -// A C comment -void main() __attribute__((noinline)); -``` -==== - -It is common to have entire files written in this manner when tight interoperability with C is implied, usually with an `.cnx` extension. diff --git a/notes/notes.json b/notes/notes.json deleted file mode 100644 index 280ae2a..0000000 --- a/notes/notes.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "tags": "note", - "layout": "note.pug" -} diff --git a/public/img/projects/jumpin_sweeties.png b/public/img/projects/jumpin_sweeties.png new file mode 100644 index 0000000..3a69da5 Binary files /dev/null and b/public/img/projects/jumpin_sweeties.png differ diff --git a/public/styles/common.css b/public/styles/common.css index 3fb2abb..57d54cf 100644 --- a/public/styles/common.css +++ b/public/styles/common.css @@ -9,3 +9,30 @@ body { font-family: 'Vollkorn', serif; width: 100%; } + +main { + display: flex; + flex-direction: column; + align-items: center; + justify-items: center; + width: 100%; + max-width: 100vw; +} + +@media (max-width: 768px) { + main { + padding: 0; + } +} + +section { + display: flex; + flex-direction: column; + align-items: center; + width: calc(100% - 1rem * 2); + padding: 2rem 1rem; +} + +section:not(:first-of-type) { + border-top: 1px solid rgba(0, 0, 0, 0.05); +} diff --git a/public/styles/cv.css b/public/styles/cv.css new file mode 100644 index 0000000..f8768f6 --- /dev/null +++ b/public/styles/cv.css @@ -0,0 +1,15 @@ +@import './common.css'; + +#about { + max-width: 42rem; +} + +big { + font-size: xx-large; + margin: 1rem 0; +} + +#about p { + margin: 1rem; + width: 100%; +} diff --git a/public/styles/index.css b/public/styles/index.css index cf215f3..ec6ce5f 100644 --- a/public/styles/index.css +++ b/public/styles/index.css @@ -1,43 +1 @@ @import './common.css'; - -main { - display: flex; - flex-direction: column; - align-items: center; - padding: 2rem 0; -} - -.posts { - /* list-style: none; */ - margin-top: 0; - padding-left: 0; -} - -.posts li { - display: grid; - grid-template-columns: 1fr 2fr; - column-gap: 0.5rem; -} - -.posts .date { - text-align: right; -} - -@media (max-width: 768px) { - main { - padding: 0; - } - - .posts { - padding-left: 2rem; - padding-right: 1rem; - } - - .posts .date { - display: none; - } - - .posts li { - display: list-item; - } -} diff --git a/public/styles/post.css b/public/styles/post.css index 50e9dc0..8913c1b 100644 --- a/public/styles/post.css +++ b/public/styles/post.css @@ -1,16 +1,10 @@ @import './common.css'; main { - display: flex; - flex-direction: column; - justify-items: center; - align-items: center; padding: 2rem 0; - width: 100%; - max-width: 100vw; } -main > h1 { +main>h1 { font-size: 3rem; line-height: 3rem; margin-bottom: 0; @@ -19,21 +13,22 @@ main > h1 { } @media (max-width: 768px) { - main > h1 { + main>h1 { font-size: 2rem; line-height: 2rem; } } -main > .meta { +main>.meta { font-style: italic; } -main > h1, main > .meta { +main>h1, +main>.meta { text-align: center; } -main > hr { +main>hr { border: none; height: 1px; background-color: lightgray; @@ -44,7 +39,11 @@ main > hr { article { width: 100%; - max-width: 35rem; + max-width: 42rem; +} + +article p { + width: 100%; } article .secret-link { @@ -57,16 +56,15 @@ article .secret-link { zoom: .6; } */ -main > h1, -main > .meta, -article > p, -article > h2, -article > h3, -article > h4, -article > h5, -article > h6, -article.asciidoctor .paragraph -{ +main>h1, +main>.meta, +article>p, +article>h2, +article>h3, +article>h4, +article>h5, +article>h6, +article.asciidoctor .paragraph { margin-left: 1rem; margin-right: 1rem; } @@ -97,10 +95,12 @@ blockquote { background-color: rgba(0, 0, 0, 0.025); } -pre[class*="language-"], pre.highlight { +pre[class*="language-"], +pre.highlight { width: 100%; box-sizing: border-box; max-width: 100vw; + font-size: 1rem !important; } pre.highlight { @@ -114,17 +114,17 @@ code { /* padding: 0 0.25rem; */ } -article.asciidoctor .exampleblock { - width: 100%; - box-sizing: border-box; - max-width: 100vw; - padding: 0.5rem 1rem; - background-color: rgba(0, 0, 0, 0.05); +.header-anchor { + opacity: 0; } -article.asciidoctor .listingblock > .title { - font-style: italic; - margin-bottom: -0.5rem; +h1:hover>.header-anchor, +h2:hover>.header-anchor, +h3:hover>.header-anchor, +h4:hover>.header-anchor, +h5:hover>.header-anchor, +h6:hover>.header-anchor { + opacity: 1; } details { @@ -187,7 +187,8 @@ table { border-spacing: 0; } -th, td { +th, +td { border-bottom: 1px solid rgba(0, 0, 0, 0.2); padding: 1rem; } diff --git a/public/styles/posts.css b/public/styles/posts.css new file mode 100644 index 0000000..8522f9d --- /dev/null +++ b/public/styles/posts.css @@ -0,0 +1,30 @@ +.posts { + /* list-style: none; */ + margin-top: 0; + padding-left: 0; +} + +.posts li { + display: grid; + grid-template-columns: 1fr 2fr; + column-gap: 0.5rem; +} + +.posts .date { + text-align: right; +} + +@media (max-width: 768px) { + .posts { + padding-left: 2rem; + padding-right: 1rem; + } + + .posts .date { + display: none; + } + + .posts li { + display: list-item; + } +} diff --git a/public/styles/projects.css b/public/styles/projects.css new file mode 100644 index 0000000..e5f1006 --- /dev/null +++ b/public/styles/projects.css @@ -0,0 +1,77 @@ +/* TODO: Improve scoping. */ +small.annotation { + text-align: center; + margin-top: -1rem; + margin-bottom: 1rem; +} + +/* TODO: Improve scoping. */ +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 1rem 0; + text-align: center; +} + +ul.projects { + --gap: 1rem; + + display: flex; + flex-wrap: wrap; + max-width: 768px; + padding: 0; + gap: var(--gap); +} + +ul.projects>li.project-card { + --border-width: 1px; + --border-radius: 4px; + --padding: 1.5rem; + + display: block; + width: calc(50% - (var(--padding) * 2) - var(--gap) / 2 - var(--border-width) * 2); + padding: var(--padding); + border-radius: var(--border-radius); + border: var(--border-width) solid rgba(0, 0, 0, 0.1) +} + +ul.projects>li.project-card>.title { + display: block; + margin-bottom: -0.25rem; + font-weight: bold; +} + +ul.projects>li.project-card>p { + margin: 0.5rem 0; +} + +ul.projects>li.project-card>p:last-child { + margin-bottom: 0; +} + +ul.projects>li.project-card .stack { + display: flex; + flex-wrap: wrap; + gap: 0.25rem; +} + +ul.projects>li.project-card .stack>span { + display: block; + padding: 0.25rem 0.5rem; + background-color: rgba(0, 0, 0, 0.1); + border-radius: 4px; + font-size: small; +} + +@media(max-width: 768px) { + ul.projects { + padding: 0 + } + + ul.projects>li.project-card { + width: 100%; + } +}