A place to cache linked articles (think custom and personal wayback machine)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.html 30KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. <!doctype html><!-- This is a valid HTML5 document. -->
  2. <!-- Screen readers, SEO, extensions and so on. -->
  3. <html lang="fr">
  4. <!-- Has to be within the first 1024 bytes, hence before the `title` element
  5. See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
  6. <meta charset="utf-8">
  7. <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
  8. <!-- The viewport meta is quite crowded and we are responsible for that.
  9. See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
  10. <meta name="viewport" content="width=device-width,initial-scale=1">
  11. <!-- Required to make a valid HTML5 document. -->
  12. <title>trapped in the technologist factory (archive) — David Larlet</title>
  13. <meta name="description" content="Publication mise en cache pour en conserver une trace.">
  14. <!-- That good ol' feed, subscribe :). -->
  15. <link rel="alternate" type="application/atom+xml" title="Feed" href="/david/log/">
  16. <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
  17. <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons2/apple-touch-icon.png">
  18. <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons2/favicon-32x32.png">
  19. <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons2/favicon-16x16.png">
  20. <link rel="manifest" href="/static/david/icons2/site.webmanifest">
  21. <link rel="mask-icon" href="/static/david/icons2/safari-pinned-tab.svg" color="#07486c">
  22. <link rel="shortcut icon" href="/static/david/icons2/favicon.ico">
  23. <meta name="msapplication-TileColor" content="#f7f7f7">
  24. <meta name="msapplication-config" content="/static/david/icons2/browserconfig.xml">
  25. <meta name="theme-color" content="#f7f7f7" media="(prefers-color-scheme: light)">
  26. <meta name="theme-color" content="#272727" media="(prefers-color-scheme: dark)">
  27. <!-- Documented, feel free to shoot an email. -->
  28. <link rel="stylesheet" href="/static/david/css/style_2021-01-20.css">
  29. <!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. -->
  30. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  31. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  32. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  33. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  34. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  35. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  36. <script>
  37. function toggleTheme(themeName) {
  38. document.documentElement.classList.toggle(
  39. 'forced-dark',
  40. themeName === 'dark'
  41. )
  42. document.documentElement.classList.toggle(
  43. 'forced-light',
  44. themeName === 'light'
  45. )
  46. }
  47. const selectedTheme = localStorage.getItem('theme')
  48. if (selectedTheme !== 'undefined') {
  49. toggleTheme(selectedTheme)
  50. }
  51. </script>
  52. <meta name="robots" content="noindex, nofollow">
  53. <meta content="origin-when-cross-origin" name="referrer">
  54. <!-- Canonical URL for SEO purposes -->
  55. <link rel="canonical" href="https://ideolalia.com/essays/trapped-in-the-technologist-factory.html">
  56. <body class="remarkdown h1-underline h2-underline h3-underline em-underscore hr-center ul-star pre-tick" data-instant-intensity="viewport-all">
  57. <article>
  58. <header>
  59. <h1>trapped in the technologist factory</h1>
  60. </header>
  61. <nav>
  62. <p class="center">
  63. <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
  64. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
  65. </svg> Accueil</a> •
  66. <a href="https://ideolalia.com/essays/trapped-in-the-technologist-factory.html" title="Lien vers le contenu original">Source originale</a>
  67. </p>
  68. </nav>
  69. <hr>
  70. <p>The first time I was paid to use Clojure was only two years after the language was released. Needless to say, it was at an <a href="https://web.archive.org/web/20101127141126/http://runa.com/">early-stage startup</a>.</p>
  71. <p>The idea behind Runa was that we’d do dynamic pricing for smaller online retailers, offering targeted discounts based on individual shopper behavior. In practice, we offered a fixed discount to anyone who abandoned their shopping cart. By treating a small percentage of customers as a control group, we could calculate our effect on the retailer’s sales, and take a cut. Our biggest problem was convincing retailers to let us control their prices, closely followed by convincing them the math behind our calculated “lift” was sound.</p>
  72. <p>We told ourselves a big contract was always just about to close, permanently increasing our traffic by an order of magnitude. We used <a href="https://hbase.apache.org/">HBase</a> as our primary data store, because it was designed to scale, just like us.<sup id="fnref:hbase" role="doc-noteref"><a href="#fn:hbase" class="footnote">1</a></sup> In the future, we would power a loosely federated Amazon, composed of independent online retailers, knit together by our software. If we built it, they would come.</p>
  73. <p>But they didn’t. The only thing that kept us afloat was a bespoke product we had built for eBay, because our CEO knew an executive there. Later, after I left, that same executive moved to Staples and convinced them to acquire the startup outright. Nothing we had built was useful to Staples, it was just evidence of our ability to “innovate”. The resulting <a href="https://web.archive.org/web/20180224081622/http://www.staples-sparx.com/">“skunk works” team</a> has since been disbanded.</p>
  74. <p>My next job was at <a href="https://web.archive.org/web/20121001101254/http://www.factual.com/">Factual</a>, a more established startup that had built their product using Java and <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino">embedded Javascript</a>, and were beginning to adopt Clojure. Their product was an index of all the places in the world, generated by applying hundreds of different heuristics to web crawl data. Each entry had a canonical id and various properties like name, address, and hours. More interestingly, it also had the equivalent ids from different platforms like Yelp and Foursquare, and we offered an API that could resolve an an incomplete (or even partially incorrect) set of properties to the corresponding entity in our index.</p>
  75. <p>Our product could stitch together thousands of disparate datasets, so long as they had some geographic component; I liked to describe it as a “foreign key for the world”. Unfortunately, there’s not much money in ontologies. By the time I had joined, the dataset had already been used as the basis for <a href="https://www.facebook.com/places/">Facebook Places</a> and as part of Apple’s then-new map offering. These were large contracts, but there are only so many big tech firms in the world, and even fewer with a geographic component to their data. No matter how much we tried to make up the difference with our smaller customers, we didn’t have a path to a billion dollar valuation.</p>
  76. <p>We began to offer services powered by our data. Internally, this was presented as less a pivot than a demonstration of the intrinsic value of our data; once people saw what <em>we</em> could do with our data, surely they’d have some ideas of their own. Our first customers were adtech firms, hoping to do better geographic targeting for mobile ads. We hired salespeople and marketing executives with a background in digital advertising, and when sales started to take off we hired more. Even as I built <a href="https://github.com/factual/s3-journal">services</a> to ingest billions of daily ad impressions and bespoke <a href="https://github.com/factual/riffle">almost-databases</a> to power on-premise services for our adtech partners, I told myself it was all incidental; our business was data, not ads.</p>
  77. <p>I was wrong, of course. The advertising side of the company was where all the growth was happening, and our product direction was whatever helped them close deals. But I wasn’t the only person who was slow to acknowledge this fact. In my exit interview with the CEO, just after I had given notice, I told him that he should be more direct with everyone that Factual’s focus had permanently shifted. He told me what I had only just stopped telling myself: ads were a temporary diversion, and we’d return to the underlying data before too long. In the years that followed, however, Factual became increasingly focused on location-based advertising, until it finally <a href="https://techcrunch.com/2020/04/06/foursquare-merges-with-factual/">merged with Foursquare</a>.</p>
  78. <hr />
  79. <p>When a startup takes venture funding, its singular goal becomes growth; it will either reach a billion dollar valuation or die trying. With that growth comes ever-greater technical challenges. Some of these are obvious: more users will not only require more machines, but a more complex infrastructure.<sup id="fnref:data" role="doc-noteref"><a href="#fn:data" class="footnote">2</a></sup> Others are less obvious: as the userbase grows, each individual becomes increasingly abstracted in the business logic of our software. To deal with the first challenge, we must have technical acumen. To deal with the second, we must have a deep understanding of our business domain.</p>
  80. <p>This means that as long as a startup continues to grow, its engineers can (and must) continue to improve their knowledge and acumen. But most startups fail. Some of those failures are because the company <a href="https://en.wikipedia.org/wiki/Friendster">couldn’t keep up with their own growth</a>, but these are the exception. The common, boring reason for failure is that the startup barely grew at all.</p>
  81. <p>Confronted with stagnation, an engineering organization can have two reactions: they can try to help reinvent the company, or they can continue to prepare for the growth that must, inevitably, come. At Runa, we chose the latter.</p>
  82. <p>At first, the engineering leadership adopted promising, poorly-understood technologies (Clojure! HBase!) which would give them the necessary leverage down the line. But once they launched, and the stagnation set in, that preparation became a need for constant readiness. Any early technical mistakes, however regrettable, were baked in. Trying to revisit them, to build something better, would just leave us flat-footed when the growth finally came.</p>
  83. <p>I joined Runa only a few months before the engineering organization entered its defensive crouch. I was too inexperienced and too distracted<sup id="fnref:depression" role="doc-noteref"><a href="#fn:depression" class="footnote">3</a></sup> to question it. Every day we played Jenga, trying to jostle the existing codebase as little as possible with each new commit. Later, as I realized I wasn’t proud of any of the work I had done so far, I began to energetically push for rebuilding key pieces of the system.</p>
  84. <p>After a few months, I was told to either drop it or take three weeks severance. I took the severance.</p>
  85. <p>Of course, even if I had gotten my way, nothing would have changed. All of us were carefully ignoring the actual problem, which is that our business model didn’t really work. Our product and pricing model both required unjustifiable levels of trust from our prospective customers, but none of us saw that as our problem to solve. We were downstream of the business model; our job was simply to wait and prepare for its eventual success.</p>
  86. <p>I joined Factual because I found the problem space fascinating, but most of it had been solved by the time I joined. About six months later, the leadership began to acknowledge that our business model, while successful, wasn’t going to get us to a billion dollar valuation. So began my descent into adtech.</p>
  87. <p>I wanted to prove that I had learned my lesson; if the current business model wasn’t working, I’d help them build a new one. I built services to consume the raw data, and on-premise servers to provide the processed result to our customers. I attended sales meetings, wrote design docs, and painstakingly analyzed packet dumps to prove to partners that latency spikes were occurring elsewhere in their system.</p>
  88. <p>In the end, I learned that I didn’t like the advertising industry very much. When a brand wanted to run a digital ad campaign, they went to an agency, which is be responsible for spending their budget to the greatest effect. The agency then went to a demand-side platform (DSP), which was be responsible for making real-time bids on individual ad impressions on an exchange. To inform those decisions, the DSP went to a company like Factual, which could distill a phone’s current and past locations into binary features relevant to the campaign.</p>
  89. <p>Crucially, when you’re not selling something online, it’s almost impossible to measure the efficacy of a digital ad campaign.<sup id="fnref:attribution" role="doc-noteref"><a href="#fn:attribution" class="footnote">4</a></sup> This means that the agency could say almost anything, so long as it closed the deal. The DSP, in turn, sold whatever they thought would help agencies close their deals. And at the end of this chain was Factual, selling whatever we thought would entice the salespeople enticing the salespeople enticing the brands.</p>
  90. <p>I thought I had become product-focused, ready to solve any problem that would help the company to succeed. In truth, I was only really paying attention to the technical problems. Once they were largely solved, it became harder to ignore my ambivalence for what the product actually did. Eventually, I left.</p>
  91. <hr />
  92. <p>I learned two lessons from this time in my life. The first was personal: I am, at heart, a technologist. I like to generalize, to abstract. While I believe it’s crucial to understand the context around your software, I’m happiest when that context is other people’s software.</p>
  93. <p>The second lesson was broader, and less obvious:<sup id="fnref:protagonist" role="doc-noteref"><a href="#fn:protagonist" class="footnote">5</a></sup> our industry is designed to foster people like me.</p>
  94. <p>This was surprising because it seems so clearly against our own interest. In almost every case, companies fail because they build the wrong thing. Unless your customers are themselves engineers, I’m the wrong person to help with that. You want someone comfortable at the periphery of your system, who wants to learn about the competitive landscape, who wants to talk to customers. You want a product engineer.</p>
  95. <p>But consider our standard interview questions: data structures, recursion, and computational complexity. It always feels a little strange when I see someone arguing that these don’t reflect “real” day-to-day software tasks; I write recursive functions all the time. But that’s a consequence of the abstraction; what might be a simple nested lookup on any specific datatype becomes recursion when you try to generalize over a set of possible datatypes.</p>
  96. <p>Likewise, I’ve seen it argued that the difference between, say, <code class="language-plaintext highlighter-rouge">O(log N)</code> and <code class="language-plaintext highlighter-rouge">O(N)</code> isn’t important, because in practice <code class="language-plaintext highlighter-rouge">N</code> tends to be small enough. That may be true for some domains, maybe even most of them, but if you’re building a general-purpose tool you have to focus on the pathological cases.</p>
  97. <p>Of course, others will argue this first-year CS material is the essence of what we do. This was certainly the attitude when I was at Google;<sup id="fnref:Google" role="doc-noteref"><a href="#fn:Google" class="footnote">6</a></sup> since their existing employees were <a href="http://www.gwtproject.org">going to any length</a> to avoid writing Javascript, they decided that anyone with frontend experience would be held to a lower technical standard. These hires were widely viewed as second-class; people who were only allowed in because they were willing to do the work no one else wanted.</p>
  98. <p>But if we want to hire product engineers, what questions should we ask instead? It’s impractical, given the sprawling scope of our industry, to only consider candidates with prior experience in our exact product space. Likewise, it’s unrealistic to expect that we’d have expertise in a candidate’s prior product spaces. We lack a common vocabulary, a common understanding of the nuances that separate good design choices from middling ones.</p>
  99. <p>And so we continue to <a href="https://en.wikipedia.org/wiki/Streetlight_effect">search for our keys under the streetlamp</a>; they could be anywhere, but this is where the light is. If we’re lucky, the technologists we hire will also have all the other skills necessary to make humane, useful software.</p>
  100. <p>This means that even if you’re not a technologist, you have to learn how to pretend.<sup id="fnref:alternatives" role="doc-noteref"><a href="#fn:alternatives" class="footnote">7</a></sup> And this brings us back to the beginning, when it was obvious that my first Clojure job, only two years after the language was released, would have been at an early-stage startup.</p>
  101. <p>Most new technologies, if they get adopted at all, follow a familiar path: first come the hobbyists, and then the companies which employ those hobbyists. This second step is easiest when the company has little to no process around adopting a new technology; in other words, an early-stage startup. The <a href="http://www.paulgraham.com/avg.html">standard narrative</a> around this phenomenon is that certain technologies are simply more powerful, but only startups are daring enough to hire from the niche pool of talent that knows how to wield them.</p>
  102. <p>But new technologies<sup id="fnref:novelty" role="doc-noteref"><a href="#fn:novelty" class="footnote">8</a></sup> don’t have power; for that they’d need a community, documentation, and a thriving ecosystem of ancillary technology. What they have is <em>potential</em>, which resonates with the potential within the startup and the early adopter; perhaps they can all, over time, grow together.</p>
  103. <p><a href="https://mcfunley.com/choose-boring-technology">As many have pointed out</a>, this is not a rational strategy for building a company. It is, however, a phenomenal way to train new technologists. <a href="https://en.wiktionary.org/wiki/Chesterton%27s_fence">Chesterton</a> notwithstanding, the fastest way to learn why a fence exists is to tear it down and see what happens.</p>
  104. <p>Seen from this perspective, it doesn’t seem so irrational; most startups fail before reaching a scale that has any existential technical risks. The only guaranteed benefit they can offer their engineers is the freedom to invent their own challenges, and learn through iterative failure.<sup id="fnref:ladder" role="doc-noteref"><a href="#fn:ladder" class="footnote">9</a></sup> If the startup fails, there’s no harm done. If the startup gets traction, the engineers can apply their newfound wisdom.</p>
  105. <p>This means startups don’t adopt new technologies despite their immaturity, they adopt them <em>because</em> of that immaturity. This drives a constant churn of novelty and obsolescence, which amplifies the importance of a technologist’s skillset, which drives startups to adopt new technologies.</p>
  106. <p>This flywheel has been spinning for a long time, and won’t stop simply because I’ve pointed out that we’re conflating novelty with technological advancement. Hopefully we can slow it down, though, because I believe it’s causing real harm.</p>
  107. <p>By introducing abstraction into every problem we solve, we distance ourselves from how our work is ultimately used. We tell ourselves we’re in the business of building sharp knives; if we made them safer, they’d be useless for everything except spreading butter. We float above the the effects of what we’ve created, treating them as inexorable consequences of <a href="https://patrickcollison.com/progress">progress</a>.</p>
  108. <p>It’s true we can’t encode our values into general-purpose software,<sup id="fnref:license" role="doc-noteref"><a href="#fn:license" class="footnote">10</a></sup> but we’re not simply atomized technologists, and our worlds are not bounded by the interfaces we expose. We share a collective responsibility for what we create, and are capable of collectively acting on that responsibility.</p>
  109. <p>But what does a belief in collective responsibility mean, in practical terms? What actions does it entail? Honestly, I don’t know. All I know is that we can’t stay under the streetlamp forever. At some point, we’ll have to see what’s out there in the dark.</p>
  110. <hr />
  111. <div class="footnotes" role="doc-endnotes">
  112. <ol>
  113. <li id="fn:hbase" role="doc-endnote">
  114. <p>In a twist that will surprise no one, the way we used it could never scale. To support ordered scans over keys, HBase partitions on contiguous ranges in the keyspace. Since we wanted to scan over the events for a given day, all of our keys began with a timestamp. This meant every write for a given time period was focused on a single shard, which would drive HBase to constantly repartition its shards in a futile attempt to equally distribute the load. Our HBase cluster was always on the cusp of falling over. <a href="#fnref:hbase" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
  115. </li>
  116. <li id="fn:data" role="doc-endnote">
  117. <p>Even in the exceedingly rare case where your core services can horizontally scale, processing the increasing volumes of data they generate will introduce complexity elsewhere in your system. <a href="#fnref:data" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
  118. </li>
  119. <li id="fn:depression" role="doc-endnote">
  120. <p>My first year at Runa coincided with a bout of mostly-unrelated depression, and the one-foot-in-front-of-the-other approach to software development perfectly mirrored how I was approaching pretty much everything else in my life. <a href="#fnref:depression" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
  121. </li>
  122. <li id="fn:attribution" role="doc-endnote">
  123. <p>The only possible exception to this is Google. If you have an Android phone, Google can attribute your visit to a Walmart store to an ad for Walmart they showed you some number of days earlier. If you use Google Pay, they can even attribute your purchases. They may not be able to prove a causal link, but it’s still lightyears beyond what anyone else has to offer. <a href="#fnref:attribution" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
  124. </li>
  125. <li id="fn:protagonist" role="doc-endnote">
  126. <p>It’s difficult to reason about structural forces in tech, and especially in startups, because of the pervading myth that we are all protagonists within the company’s narrative arc. If something failed, it’s only because we didn’t work hard enough, or didn’t have a better idea. Even now, years later, thinking about this period in my career brings a twinge of guilt; couldn’t I have changed how the story ended? <a href="#fnref:protagonist" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
  127. </li>
  128. <li id="fn:Google" role="doc-endnote">
  129. <p>This was back in 2010, but I’d be surprised if anything has substantially changed. <a href="#fnref:Google" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
  130. </li>
  131. <li id="fn:alternatives" role="doc-endnote">
  132. <p>I know of a few ways to skip this gauntlet, but none of them have universal appeal. If someone at an early-stage startup vouches for you, they’ll often skip straight to making sure you’re a “culture fit”. This only works, however, for a candidate with a strong network, who wants to work for a small startup, and who fits the culture in question. Likewise, you can simply found your own company, but being a founder places you first and foremost at the interface between business and investor; the interface between product and user is always further down the list. <a href="#fnref:alternatives" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
  133. </li>
  134. <li id="fn:novelty" role="doc-endnote">
  135. <p>And the niche technologies adopted by startups are, almost without exception, new. Graham’s Lisp evangelism didn’t lead to widespread adoption of Common Lisp, it led to the adoption of Clojure, a brand new Lisp which was still full of potential. <a href="#fnref:novelty" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
  136. </li>
  137. <li id="fn:ladder" role="doc-endnote">
  138. <p>Given our current situation, telling junior engineers to only use boring technologies can actually be portrayed as the older generations of engineers pulling up the ladder. They got to learn through failure, why can’t the new generation do the same? <a href="#fnref:ladder" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
  139. </li>
  140. <li id="fn:license" role="doc-endnote">
  141. <p>I’m begging everyone who thinks a software license can delineate when something is or isn’t being used for a bad purpose to read a single book on ethics. <a href="#fnref:license" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
  142. </li>
  143. </ol>
  144. </div>
  145. </article>
  146. <hr>
  147. <footer>
  148. <p>
  149. <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
  150. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
  151. </svg> Accueil</a> •
  152. <a href="/david/log/" title="Accès au flux RSS"><svg class="icon icon-rss2">
  153. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-rss2"></use>
  154. </svg> Suivre</a> •
  155. <a href="http://larlet.com" title="Go to my English profile" data-instant><svg class="icon icon-user-tie">
  156. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-user-tie"></use>
  157. </svg> Pro</a> •
  158. <a href="mailto:david%40larlet.fr" title="Envoyer un courriel"><svg class="icon icon-mail">
  159. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-mail"></use>
  160. </svg> Email</a> •
  161. <abbr class="nowrap" title="Hébergeur : Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33184162340"><svg class="icon icon-hammer2">
  162. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-hammer2"></use>
  163. </svg> Légal</abbr>
  164. </p>
  165. <template id="theme-selector">
  166. <form>
  167. <fieldset>
  168. <legend><svg class="icon icon-brightness-contrast">
  169. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-brightness-contrast"></use>
  170. </svg> Thème</legend>
  171. <label>
  172. <input type="radio" value="auto" name="chosen-color-scheme" checked> Auto
  173. </label>
  174. <label>
  175. <input type="radio" value="dark" name="chosen-color-scheme"> Foncé
  176. </label>
  177. <label>
  178. <input type="radio" value="light" name="chosen-color-scheme"> Clair
  179. </label>
  180. </fieldset>
  181. </form>
  182. </template>
  183. </footer>
  184. <script src="/static/david/js/instantpage-5.1.0.min.js" type="module"></script>
  185. <script>
  186. function loadThemeForm(templateName) {
  187. const themeSelectorTemplate = document.querySelector(templateName)
  188. const form = themeSelectorTemplate.content.firstElementChild
  189. themeSelectorTemplate.replaceWith(form)
  190. form.addEventListener('change', (e) => {
  191. const chosenColorScheme = e.target.value
  192. localStorage.setItem('theme', chosenColorScheme)
  193. toggleTheme(chosenColorScheme)
  194. })
  195. const selectedTheme = localStorage.getItem('theme')
  196. if (selectedTheme && selectedTheme !== 'undefined') {
  197. form.querySelector(`[value="${selectedTheme}"]`).checked = true
  198. }
  199. }
  200. const prefersColorSchemeDark = '(prefers-color-scheme: dark)'
  201. window.addEventListener('load', () => {
  202. let hasDarkRules = false
  203. for (const styleSheet of Array.from(document.styleSheets)) {
  204. let mediaRules = []
  205. for (const cssRule of styleSheet.cssRules) {
  206. if (cssRule.type !== CSSRule.MEDIA_RULE) {
  207. continue
  208. }
  209. // WARNING: Safari does not have/supports `conditionText`.
  210. if (cssRule.conditionText) {
  211. if (cssRule.conditionText !== prefersColorSchemeDark) {
  212. continue
  213. }
  214. } else {
  215. if (cssRule.cssText.startsWith(prefersColorSchemeDark)) {
  216. continue
  217. }
  218. }
  219. mediaRules = mediaRules.concat(Array.from(cssRule.cssRules))
  220. }
  221. // WARNING: do not try to insert a Rule to a styleSheet you are
  222. // currently iterating on, otherwise the browser will be stuck
  223. // in a infinite loop…
  224. for (const mediaRule of mediaRules) {
  225. styleSheet.insertRule(mediaRule.cssText)
  226. hasDarkRules = true
  227. }
  228. }
  229. if (hasDarkRules) {
  230. loadThemeForm('#theme-selector')
  231. }
  232. })
  233. </script>
  234. </body>
  235. </html>